Skip to content

Commit e22a7e6

Browse files
authored
Add Voici directive (#100)
* Add Voici directive * Linter * Add directive * Install voici in dev env * Update docs * Fix iframe path * Run black * Missing return statement * Try forcing building voici * Be more clever choosing which app to build * Fix notebook path for voici * Linter + syntax issue * Fix variable shadowing * Allow for tree * Add tree example in docs
1 parent b19bf57 commit e22a7e6

File tree

4 files changed

+150
-27
lines changed

4 files changed

+150
-27
lines changed

dev-environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ dependencies:
1414
- black
1515
- pip:
1616
- .
17+
- voici

docs/directives/voici.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Voici directive
2+
3+
`jupyterlite-sphinx` provides a `voici` directive that allows you to embed a [voici dashboard](https://github.com/voila-dashboards/voici) in your docs.
4+
5+
```rst
6+
.. voici::
7+
:height: 600px
8+
```
9+
10+
```{eval-rst}
11+
.. voici::
12+
:height: 600px
13+
```
14+
15+
You can provide a notebook that will be rendered with Voici:
16+
17+
```rst
18+
.. voici:: my_notebook.ipynb
19+
:height: 600px
20+
:prompt: Try Voici!
21+
:prompt_color: #dc3545
22+
```
23+
24+
```{eval-rst}
25+
.. voici:: my_notebook.ipynb
26+
:height: 600px
27+
:prompt: Try Voici!
28+
:prompt_color: #dc3545
29+
```

docs/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Each of those directives can be configured with the following options:
3939
directives/jupyterlite
4040
directives/retrolite
4141
directives/replite
42+
directives/voici
4243
full
4344
changelog
4445
```

jupyterlite_sphinx/jupyterlite_sphinx.py

Lines changed: 119 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323

2424
CONTENT_DIR = "_contents"
2525
JUPYTERLITE_DIR = "lite"
26+
# Using a global variable, is there a better way?
27+
APPS = []
2628

2729

2830
# Used for nodes that do not need to be rendered
@@ -36,52 +38,29 @@ def visit_element_html(self, node):
3638
raise SkipNode
3739

3840

39-
class _LiteIframe(Element):
41+
class _PromptedIframe(Element):
4042
def __init__(
4143
self,
4244
rawsource="",
4345
*children,
44-
prefix=JUPYTERLITE_DIR,
46+
iframe_src="",
4547
width="100%",
4648
height="100%",
4749
prompt=False,
4850
prompt_color=None,
49-
content=[],
50-
notebook=None,
51-
lite_options={},
5251
**attributes,
5352
):
5453
super().__init__(
5554
"",
56-
prefix=prefix,
55+
iframe_src=iframe_src,
5756
width=width,
5857
height=height,
5958
prompt=prompt,
6059
prompt_color=prompt_color,
61-
content=content,
62-
notebook=notebook,
63-
lite_options=lite_options,
6460
)
6561

6662
def html(self):
67-
lite_options = self["lite_options"]
68-
69-
if self["content"]:
70-
code_lines = ["" if not line.strip() else line for line in self["content"]]
71-
code = "\n".join(code_lines)
72-
73-
lite_options["code"] = code
74-
75-
app_path = self.lite_app
76-
if self["notebook"] is not None:
77-
lite_options["path"] = self["notebook"]
78-
app_path = f"{self.lite_app}{self.notebooks_path}"
79-
80-
options = "&".join(
81-
[f"{key}={quote(value)}" for key, value in lite_options.items()]
82-
)
83-
84-
iframe_src = f'{self["prefix"]}/{app_path}{f"?{options}" if options else ""}'
63+
iframe_src = self["iframe_src"]
8564

8665
if self["prompt"]:
8766
prompt = (
@@ -108,6 +87,37 @@ def html(self):
10887
)
10988

11089

90+
class _LiteIframe(_PromptedIframe):
91+
def __init__(
92+
self,
93+
rawsource="",
94+
*children,
95+
prefix=JUPYTERLITE_DIR,
96+
content=[],
97+
notebook=None,
98+
lite_options={},
99+
**attributes,
100+
):
101+
if content:
102+
code_lines = ["" if not line.strip() else line for line in content]
103+
code = "\n".join(code_lines)
104+
105+
lite_options["code"] = code
106+
107+
app_path = self.lite_app
108+
if notebook is not None:
109+
lite_options["path"] = notebook
110+
app_path = f"{self.lite_app}{self.notebooks_path}"
111+
112+
options = "&".join(
113+
[f"{key}={quote(value)}" for key, value in lite_options.items()]
114+
)
115+
116+
iframe_src = f'{prefix}/{app_path}{f"?{options}" if options else ""}'
117+
118+
super().__init__(rawsource, *children, iframe_src=iframe_src, **attributes)
119+
120+
111121
class RepliteIframe(_LiteIframe):
112122
"""Appended to the doctree by the RepliteDirective directive
113123
@@ -138,6 +148,35 @@ class RetroLiteIframe(_LiteIframe):
138148
notebooks_path = "notebooks/"
139149

140150

151+
class VoiciIframe(_PromptedIframe):
152+
"""Appended to the doctree by the VoiciDirective directive
153+
154+
Renders an iframe that shows a Notebook with Voici.
155+
"""
156+
157+
def __init__(
158+
self,
159+
rawsource="",
160+
*children,
161+
prefix=JUPYTERLITE_DIR,
162+
notebook=None,
163+
lite_options={},
164+
**attributes,
165+
):
166+
if notebook is not None:
167+
app_path = f"voici/render/{notebook.replace('.ipynb', '.html')}"
168+
else:
169+
app_path = "voici/tree"
170+
171+
options = "&".join(
172+
[f"{key}={quote(value)}" for key, value in lite_options.items()]
173+
)
174+
175+
iframe_src = f'{prefix}/{app_path}{f"?{options}" if options else ""}'
176+
177+
super().__init__(rawsource, *children, iframe_src=iframe_src, **attributes)
178+
179+
141180
class RepliteDirective(SphinxDirective):
142181
"""The ``.. replite::`` directive.
143182
@@ -157,6 +196,9 @@ class RepliteDirective(SphinxDirective):
157196
}
158197

159198
def run(self):
199+
if not "repl" in APPS:
200+
APPS.append("repl")
201+
160202
width = self.options.pop("width", "100%")
161203
height = self.options.pop("height", "100%")
162204

@@ -247,6 +289,12 @@ class JupyterLiteDirective(_LiteDirective):
247289

248290
iframe_cls = JupyterLiteIframe
249291

292+
def run(self):
293+
if not "lab" in APPS:
294+
APPS.append("lab")
295+
296+
return super().run()
297+
250298

251299
class RetroLiteDirective(_LiteDirective):
252300
"""The ``.. retrolite::`` directive.
@@ -256,6 +304,34 @@ class RetroLiteDirective(_LiteDirective):
256304

257305
iframe_cls = RetroLiteIframe
258306

307+
def run(self):
308+
if not "retro" in APPS:
309+
APPS.append("retro")
310+
311+
return super().run()
312+
313+
314+
class VoiciDirective(_LiteDirective):
315+
"""The ``.. voici::`` directive.
316+
317+
Renders a Notebook with Voici in the docs.
318+
"""
319+
320+
iframe_cls = VoiciIframe
321+
322+
def run(self):
323+
try:
324+
import voici
325+
except ImportError:
326+
raise RuntimeError(
327+
"Voici must be installed if you want to make use of the voici directive: pip install voici"
328+
)
329+
330+
if not "voici" in APPS:
331+
APPS.append("voici")
332+
333+
return super().run()
334+
259335

260336
class RetroLiteParser(RSTParser):
261337
"""Sphinx source parser for Jupyter notebooks.
@@ -317,12 +393,17 @@ def jupyterlite_build(app: Sphinx, error):
317393
for content in jupyterlite_contents:
318394
contents.extend(["--contents", content])
319395

396+
apps = []
397+
for jlite_app in APPS:
398+
apps.extend(["--apps", jlite_app])
399+
320400
command = [
321401
"jupyter",
322402
"lite",
323403
"build",
324404
"--debug",
325405
*config,
406+
*apps,
326407
*contents,
327408
"--contents",
328409
os.path.join(app.srcdir, CONTENT_DIR),
@@ -389,6 +470,17 @@ def setup(app):
389470
)
390471
app.add_directive("replite", RepliteDirective)
391472

473+
# Initialize Voici directive
474+
app.add_node(
475+
VoiciIframe,
476+
html=(visit_element_html, None),
477+
latex=(skip, None),
478+
textinfo=(skip, None),
479+
text=(skip, None),
480+
man=(skip, None),
481+
)
482+
app.add_directive("voici", VoiciDirective)
483+
392484
# CSS and JS assets
393485
copy_asset(str(HERE / "jupyterlite_sphinx.css"), str(Path(app.outdir) / "_static"))
394486
copy_asset(str(HERE / "jupyterlite_sphinx.js"), str(Path(app.outdir) / "_static"))

0 commit comments

Comments
 (0)