3
3
import os
4
4
from itertools import groupby , count
5
5
from operator import itemgetter
6
+ import json
6
7
7
8
from sphinx .util import logging
8
9
from sphinx .transforms import SphinxTransform
23
24
24
25
import nbformat
25
26
27
+ from ipywidgets import Widget
28
+ import ipywidgets .embed
26
29
27
30
from ._version import __version__
28
31
32
+
29
33
logger = logging .getLogger (__name__ )
30
34
35
+ WIDGET_VIEW_MIMETYPE = 'application/vnd.jupyter.widget-view+json'
36
+ WIDGET_STATE_MIMETYPE = 'application/vnd.jupyter.widget-state+json'
37
+ REQUIRE_URL_DEFAULT = 'https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.4/require.min.js'
38
+
39
+
40
+ def builder_inited (app ):
41
+ """
42
+ 2 cases
43
+ case 1: ipywidgets 7, with require
44
+ case 2: ipywidgets 7, no require
45
+ """
46
+ require_url = app .config .jupyter_sphinx_require_url
47
+ if require_url :
48
+ app .add_js_file (require_url )
49
+ embed_url = app .config .jupyter_sphinx_embed_url or ipywidgets .embed .DEFAULT_EMBED_REQUIREJS_URL
50
+ else :
51
+ embed_url = app .config .jupyter_sphinx_embed_url or ipywidgets .embed .DEFAULT_EMBED_SCRIPT_URL
52
+ if embed_url :
53
+ app .add_js_file (embed_url )
54
+
31
55
32
56
### Directives and their associated doctree nodes
33
57
@@ -167,6 +191,47 @@ class JupyterCellNode(docutils.nodes.container):
167
191
"""
168
192
169
193
194
+ class JupyterWidgetViewNode (docutils .nodes .Element ):
195
+ """Inserted into doctree whenever a Jupyter cell produces a widget as output.
196
+
197
+ Contains a unique ID for this widget; enough information for the widget
198
+ embedding javascript to render it, given the widget state. For non-HTML
199
+ outputs this doctree node is rendered generically.
200
+ """
201
+
202
+ def __init__ (self , view_spec ):
203
+ super ().__init__ ('' , view_spec = view_spec )
204
+
205
+ def html (self ):
206
+ return ipywidgets .embed .widget_view_template .format (
207
+ view_spec = json .dumps (self ['view_spec' ]))
208
+
209
+ def text (self ):
210
+ return '[ widget ]'
211
+
212
+
213
+ class JupyterWidgetStateNode (docutils .nodes .Element ):
214
+ """Appended to doctree if any Jupyter cell produced a widget as output.
215
+
216
+ Contains the state needed to render a collection of Jupyter widgets.
217
+
218
+ Per doctree there is 1 JupyterWidgetStateNode per kernel that produced
219
+ Jupyter widgets when running. This is fine as (presently) the
220
+ 'html-manager' Javascript library, which embeds widgets, loads the state
221
+ from all script tags on the page of the correct mimetype.
222
+ """
223
+
224
+ def __init__ (self , state ):
225
+ super ().__init__ ('' , state = state )
226
+
227
+ def html (self ):
228
+ # TODO: render into a separate file if 'html-manager' starts fully
229
+ # parsing script tags, and not just grabbing their innerHTML
230
+ # https://github.com/jupyter-widgets/ipywidgets/blob/master/packages/html-manager/src/libembed.ts#L36
231
+ return ipywidgets .embed .snippet_template .format (
232
+ load = '' , widget_views = '' , json_data = json .dumps (self ['state' ]))
233
+
234
+
170
235
### Doctree transformations
171
236
172
237
class ExecuteJupyterCells (SphinxTransform ):
@@ -246,6 +311,9 @@ def apply(self):
246
311
)
247
312
attach_outputs (output_nodes , node )
248
313
314
+ if contains_widgets (notebook ):
315
+ doctree .append (JupyterWidgetStateNode (get_widgets (notebook )))
316
+
249
317
250
318
### Roles
251
319
@@ -364,6 +432,8 @@ def cell_output_to_nodes(cell, data_priority, dir):
364
432
text = data ,
365
433
rawsource = data ,
366
434
))
435
+ elif mime_type == WIDGET_VIEW_MIMETYPE :
436
+ to_add .append (JupyterWidgetViewNode (data ))
367
437
368
438
return to_add
369
439
@@ -398,6 +468,27 @@ def execute_cells(kernel_name, cells, execute_kwargs):
398
468
return notebook
399
469
400
470
471
+ def get_widgets (notebook ):
472
+ try :
473
+ return notebook .metadata .widgets [WIDGET_STATE_MIMETYPE ]
474
+ except AttributeError :
475
+ # Don't catch KeyError, as it's a bug if 'widgets' does
476
+ # not contain 'WIDGET_STATE_MIMETYPE'
477
+ return None
478
+
479
+
480
+ def contains_widgets (notebook ):
481
+ widgets = get_widgets (notebook )
482
+ return widgets and widgets ['state' ]
483
+
484
+
485
+ def language_info (executor ):
486
+ # Can only run this function inside 'setup_preprocessor'
487
+ assert hasattr (executor , 'kc' )
488
+ info_msg = executor ._wait_for_reply (executor .kc .kernel_info ())
489
+ return info_msg ['content' ]['language_info' ]
490
+
491
+
401
492
def write_notebook_output (notebook , output_dir , notebook_name ):
402
493
"""Extract output from notebook cells and write to files in output_dir.
403
494
@@ -455,7 +546,7 @@ def setup(app):
455
546
# Configuration
456
547
app .add_config_value (
457
548
'jupyter_execute_kwargs' ,
458
- dict (timeout = - 1 , allow_errors = True ),
549
+ dict (timeout = - 1 , allow_errors = True , store_widget_state = True ),
459
550
'env'
460
551
)
461
552
app .add_config_value (
@@ -466,6 +557,7 @@ def setup(app):
466
557
app .add_config_value (
467
558
'jupyter_execute_data_priority' ,
468
559
[
560
+ WIDGET_VIEW_MIMETYPE ,
469
561
'text/html' ,
470
562
'image/svg+xml' ,
471
563
'image/png' ,
@@ -476,6 +568,10 @@ def setup(app):
476
568
'env' ,
477
569
)
478
570
571
+ # ipywidgets config
572
+ app .add_config_value ('jupyter_sphinx_require_url' , REQUIRE_URL_DEFAULT , 'html' )
573
+ app .add_config_value ('jupyter_sphinx_embed_url' , None , 'html' )
574
+
479
575
# JupyterKernelNode is just a doctree marker for the
480
576
# ExecuteJupyterCells transform, so we don't actually render it.
481
577
def skip (self , node ):
@@ -507,6 +603,35 @@ def skip(self, node):
507
603
man = render_container ,
508
604
)
509
605
606
+ # JupyterWidgetViewNode holds widget view JSON,
607
+ # but is only rendered properly in HTML documents.
608
+ def visit_widget_html (self , node ):
609
+ self .body .append (node .html ())
610
+ raise docutils .nodes .SkipNode
611
+
612
+ def visit_widget_text (self , node ):
613
+ self .body .append (node .text ())
614
+ raise docutils .nodes .SkipNode
615
+
616
+ app .add_node (
617
+ JupyterWidgetViewNode ,
618
+ html = (visit_widget_html , None ),
619
+ latex = (visit_widget_text , None ),
620
+ textinfo = (visit_widget_text , None ),
621
+ text = (visit_widget_text , None ),
622
+ man = (visit_widget_text , None ),
623
+ )
624
+ # JupyterWidgetStateNode holds the widget state JSON,
625
+ # but is only rendered in HTML documents.
626
+ app .add_node (
627
+ JupyterWidgetStateNode ,
628
+ html = (visit_widget_html , None ),
629
+ latex = (skip , None ),
630
+ textinfo = (skip , None ),
631
+ text = (skip , None ),
632
+ man = (skip , None ),
633
+ )
634
+
510
635
app .add_directive ('jupyter-execute' , JupyterCell )
511
636
app .add_directive ('jupyter-kernel' , JupyterKernel )
512
637
app .add_role ('jupyter-download:notebook' , jupyter_download_role )
@@ -517,6 +642,8 @@ def skip(self, node):
517
642
app .add_lexer ('ipythontb' , IPythonTracebackLexer ())
518
643
app .add_lexer ('ipython' , IPython3Lexer ())
519
644
645
+ app .connect ('builder-inited' , builder_inited )
646
+
520
647
return {
521
648
'version' : __version__ ,
522
649
'parallel_read_safe' : True ,
0 commit comments