Skip to content

Commit 308034d

Browse files
authored
Merge pull request #118 from steppi/disable
Add option to disable TryExamples without rebuilding docs
2 parents 02d5811 + b2fc6db commit 308034d

File tree

3 files changed

+141
-11
lines changed

3 files changed

+141
-11
lines changed

docs/directives/try_examples.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,41 @@ allowing for specification of examples sections which should not be made interac
166166
If you are using the `TryExamples` directive in your documentation, you'll need to ensure
167167
that the version of the package installed in the Jupyterlite kernel you are using
168168
matches that of the version you are documenting.
169+
170+
## Configuration without rebuilding
171+
172+
The `TryExamples` directive supports disabling interactive examples without rebuilding
173+
the documentation. This can be helpful for projects requiring substantial documentation
174+
build time. Users may add a json config file entitled `.try_examples.json` to the root
175+
directory of the build directory for the deployed documentation. The format is a list of
176+
[JavaScript Regex patterns](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions) attached to the key `"ignore_patterns"` like below.
177+
178+
```json
179+
{
180+
"ignore_patterns": ["^/latest/.*", "^/stable/reference/generated/example.html"]
181+
}
182+
```
183+
184+
`TryExamples` buttons will be hidden in url pathnames matching at least one of these
185+
patterns, effectively disabling the interactive documentation. In the provided example:
186+
187+
* The pattern `".*latest/.*" disables interactive examples for urls for the documentation
188+
for the latest version of the package, which may be useful if this documentation is
189+
for a development version for which a corresponding package build is not available
190+
in a Jupyterlite kernel.
191+
192+
* The pattern `".*stable/reference/generated/example.html"` targets a particular url
193+
in the documentation for the latest stable release.
194+
195+
Note that these patterns should match the [pathname](https://developer.mozilla.org/en-US/docs/Web/API/Location/pathname) of the url, not the full url. This is the path portion of
196+
the url. For instance, the pathname of https://jupyterlite-sphinx.readthedocs.io/en/latest/directives/try_examples.html is `/en/latest/directives/try_examples.html`.
197+
198+
199+
A default configuration file can be specified in `conf.py` with the option
200+
`try_examples_default_runtime_config`.
201+
202+
```python
203+
try_examples_default_runtime_config = {
204+
"ignore_patterns": ["^/latest/.*", "^/stable/reference/generated/example.html"]
205+
}
206+
```

jupyterlite_sphinx/jupyterlite_sphinx.js

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,18 @@ window.tryExamplesShowIframe = (
5252
let iframe = iframeContainer.querySelector('iframe.jupyterlite_sphinx_raw_iframe');
5353

5454
if (!iframe) {
55-
const examples = examplesContainer.querySelector('.try_examples_content');
56-
iframe = document.createElement('iframe');
57-
iframe.src = iframeSrc;
58-
iframe.style.width = '100%';
55+
const examples = examplesContainer.querySelector('.try_examples_content');
56+
iframe = document.createElement('iframe');
57+
iframe.src = iframeSrc;
58+
iframe.style.width = '100%';
5959
minHeight = parseInt(iframeMinHeight);
60-
height = Math.max(minHeight, examples.offsetHeight);
61-
iframe.style.height = `${height}px`;
62-
iframe.classList.add('jupyterlite_sphinx_raw_iframe');
63-
examplesContainer.classList.add("hidden");
64-
iframeContainer.appendChild(iframe);
60+
height = Math.max(minHeight, examples.offsetHeight);
61+
iframe.style.height = `${height}px`;
62+
iframe.classList.add('jupyterlite_sphinx_raw_iframe');
63+
examplesContainer.classList.add("hidden");
64+
iframeContainer.appendChild(iframe);
6565
} else {
66-
examplesContainer.classList.add("hidden");
66+
examplesContainer.classList.add("hidden");
6767
}
6868
iframeParentContainer.classList.remove("hidden");
6969
}
@@ -76,3 +76,57 @@ window.tryExamplesHideIframe = (examplesContainerId, iframeParentContainerId) =>
7676
iframeParentContainer.classList.add("hidden");
7777
examplesContainer.classList.remove("hidden");
7878
}
79+
80+
81+
window.loadTryExamplesConfig = async (ignoreFilePath) => {
82+
try {
83+
// Add a timestamp as query parameter to ensure a cached version of the
84+
// file is not used.
85+
const timestamp = new Date().getTime();
86+
const ignoreFileUrl = `${ignoreFilePath}?cb=${timestamp}`;
87+
const currentPageUrl = window.location.pathname;
88+
89+
const response = await fetch(ignoreFileUrl);
90+
if (!response.ok) {
91+
if (response.status === 404) {
92+
// Try examples ignore file is not present.
93+
console.log('Ignore file not found.');
94+
return;
95+
}
96+
throw new Error(`Error fetching ${ignoreFilePath}`);
97+
}
98+
99+
const data = await response.json();
100+
if (!data) {
101+
return;
102+
}
103+
104+
// Disable interactive examples if file matches one of the ignore patterns
105+
// by hiding try_examples_buttons.
106+
Patterns = data.ignore_patterns;
107+
for (let pattern of Patterns) {
108+
let regex = new RegExp(pattern);
109+
if (regex.test(currentPageUrl)) {
110+
var buttons = document.getElementsByClassName('try_examples_button');
111+
for (var i = 0; i < buttons.length; i++) {
112+
buttons[i].classList.add('hidden');
113+
}
114+
break;
115+
}
116+
}
117+
} catch (error) {
118+
console.error(error);
119+
}
120+
};
121+
122+
123+
window.toggleTryExamplesButtons = () => {
124+
/* Toggle visibility of TryExamples buttons. For use in console for debug
125+
* purposes. */
126+
var buttons = document.getElementsByClassName('try_examples_button');
127+
128+
for (var i = 0; i < buttons.length; i++) {
129+
buttons[i].classList.toggle('hidden');
130+
}
131+
132+
};

jupyterlite_sphinx/jupyterlite_sphinx.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,18 @@ def run(self):
530530
"", f"<style>{complete_button_css}</style>", format="html"
531531
)
532532

533-
return [content_container_node, notebook_container, style_tag]
533+
# Search cnofig file allowing for config changes without rebuilding docs.
534+
config_path = os.path.join(relative_path_to_root, ".try_examples.json")
535+
script_html = (
536+
"<script>"
537+
'document.addEventListener("DOMContentLoaded", function() {'
538+
f'window.loadTryExamplesConfig("{config_path}");'
539+
"});"
540+
"</script>"
541+
)
542+
script_node = nodes.raw("", script_html, format="html")
543+
544+
return [content_container_node, notebook_container, style_tag, script_node]
534545

535546

536547
def _process_docstring_examples(app, docname, source):
@@ -563,6 +574,25 @@ def conditional_process_examples(app, config):
563574
app.connect("autodoc-process-docstring", _process_autodoc_docstrings)
564575

565576

577+
def write_try_examples_runtime_config(app, exception):
578+
"""Add default runtime configuration file .try_examples.json.
579+
580+
These configuration options can be changed in the deployed docs without
581+
rebuilding by replacing this file.
582+
"""
583+
if exception is not None:
584+
return
585+
586+
config = app.config.try_examples_default_runtime_config
587+
if config is None:
588+
return
589+
590+
output_dir = app.outdir
591+
config_path = os.path.join(output_dir, ".try_examples.json")
592+
with open(config_path, "w") as f:
593+
json.dump(config, f, indent=4)
594+
595+
566596
def inited(app: Sphinx, config):
567597
# Create the content dir
568598
os.makedirs(os.path.join(app.srcdir, CONTENT_DIR), exist_ok=True)
@@ -648,6 +678,9 @@ def setup(app):
648678
# We need to build JupyterLite at the end, when all the content was created
649679
app.connect("build-finished", jupyterlite_build)
650680

681+
# Write default .try_examples.json after build finishes.
682+
app.connect("build-finished", write_try_examples_runtime_config)
683+
651684
# Config options
652685
app.add_config_value("jupyterlite_config", None, rebuild="html")
653686
app.add_config_value("jupyterlite_dir", app.srcdir, rebuild="html")
@@ -672,6 +705,11 @@ def setup(app):
672705
default=None,
673706
rebuild="html",
674707
)
708+
app.add_config_value(
709+
"try_examples_default_runtime_config",
710+
default=None,
711+
rebuild=None,
712+
)
675713

676714
# Initialize NotebookLite and JupyterLite directives
677715
app.add_node(

0 commit comments

Comments
 (0)