5
5
6
6
import docutils
7
7
from docutils .parsers .rst import Directive , directives
8
- from docutils .nodes import math_block
8
+ from docutils .nodes import math_block , image
9
9
from sphinx .util import parselinenos
10
10
from sphinx .addnodes import download_reference
11
+ from sphinx .transforms import SphinxTransform
12
+ from sphinx .environment .collectors .asset import ImageCollector
11
13
12
14
import ipywidgets .embed
13
15
import nbconvert
@@ -127,20 +129,24 @@ def run(self):
127
129
else :
128
130
hl_lines = []
129
131
130
- return [
131
- JupyterCellNode (
132
- "" ,
133
- docutils .nodes .literal_block (text = "\n " .join (content )),
134
- hide_code = ("hide-code" in self .options ),
135
- hide_output = ("hide-output" in self .options ),
136
- code_below = ("code-below" in self .options ),
137
- linenos = ("linenos" in self .options ),
138
- linenostart = (self .options .get ("lineno-start" )),
139
- emphasize_lines = hl_lines ,
140
- raises = self .options .get ("raises" ),
141
- stderr = ("stderr" in self .options ),
142
- )
143
- ]
132
+ # A top-level placeholder for our cell
133
+ cell_node = JupyterCellNode (
134
+ hide_code = ("hide-code" in self .options ),
135
+ hide_output = ("hide-output" in self .options ),
136
+ code_below = ("code-below" in self .options ),
137
+ linenos = ("linenos" in self .options ),
138
+ linenostart = (self .options .get ("lineno-start" )),
139
+ emphasize_lines = hl_lines ,
140
+ raises = self .options .get ("raises" ),
141
+ stderr = ("stderr" in self .options ),
142
+ classes = ["jupyter_cell" ],
143
+ )
144
+
145
+ # Add the input section of the cell, we'll add output at execution time
146
+ cell_input = CellInputNode (classes = ["cell_input" ])
147
+ cell_input += docutils .nodes .literal_block (text = "\n " .join (content ))
148
+ cell_node += cell_input
149
+ return [cell_node ]
144
150
145
151
146
152
class JupyterCellNode (docutils .nodes .container ):
@@ -151,6 +157,28 @@ class JupyterCellNode(docutils.nodes.container):
151
157
"""
152
158
153
159
160
+ class CellInputNode (docutils .nodes .container ):
161
+ """Represent an input cell in the Sphinx AST."""
162
+
163
+ def __init__ (self , rawsource = "" , * children , ** attributes ):
164
+ super ().__init__ ("" , ** attributes )
165
+
166
+
167
+ class CellOutputNode (docutils .nodes .container ):
168
+ """Represent an output cell in the Sphinx AST."""
169
+
170
+ def __init__ (self , rawsource = "" , * children , ** attributes ):
171
+ super ().__init__ ("" , ** attributes )
172
+
173
+
174
+ class CellOutputBundleNode (docutils .nodes .container ):
175
+ """Represent a MimeBundle in the Sphinx AST, to be transformed later."""
176
+
177
+ def __init__ (self , outputs , rawsource = "" , * children , ** attributes ):
178
+ self .outputs = outputs
179
+ super ().__init__ ("" , ** attributes )
180
+
181
+
154
182
class JupyterKernelNode (docutils .nodes .Element ):
155
183
"""Inserted into doctree whenever a JupyterKernel directive is encountered.
156
184
@@ -199,12 +227,12 @@ def html(self):
199
227
)
200
228
201
229
202
- def cell_output_to_nodes (cell , data_priority , write_stderr , dir , thebe_config ):
230
+ def cell_output_to_nodes (outputs , data_priority , write_stderr , dir , thebe_config ):
203
231
"""Convert a jupyter cell with outputs and filenames to doctree nodes.
204
232
205
233
Parameters
206
234
----------
207
- cell : jupyter cell
235
+ outputs : a list of outputs from a Jupyter cell
208
236
data_priority : list of mime types
209
237
Which media types to prioritize.
210
238
write_stderr : bool
@@ -214,9 +242,14 @@ def cell_output_to_nodes(cell, data_priority, write_stderr, dir, thebe_config):
214
242
to the source folder prefixed with ``/``.
215
243
thebe_config: dict
216
244
Thebelab configuration object or None
245
+
246
+ Returns
247
+ -------
248
+ to_add : list of docutils nodes
249
+ Each output, converted into a docutils node.
217
250
"""
218
251
to_add = []
219
- for _ , output in enumerate ( cell . get ( " outputs" , [])) :
252
+ for output in outputs :
220
253
output_type = output ["output_type" ]
221
254
if output_type == "stream" :
222
255
if output ["name" ] == "stderr" :
@@ -325,33 +358,39 @@ def cell_output_to_nodes(cell, data_priority, write_stderr, dir, thebe_config):
325
358
326
359
def attach_outputs (output_nodes , node , thebe_config , cm_language ):
327
360
if not node .attributes ["hide_code" ]: # only add css if code is displayed
328
- node .attributes ["classes" ] = ["jupyter_container" ]
361
+ classes = node .attributes .get ("classes" , [])
362
+ classes += ["jupyter_container" ]
363
+
364
+ (input_node ,) = node .traverse (CellInputNode )
365
+ (outputbundle_node ,) = node .traverse (CellOutputBundleNode )
366
+ output_node = CellOutputNode (classes = ["cell_output" ])
329
367
if thebe_config :
330
- source = node .children [0 ]
368
+ # Move the source from the input node into the thebe_source node
369
+ source = input_node .children .pop (0 )
331
370
thebe_source = ThebeSourceNode (
332
371
hide_code = node .attributes ["hide_code" ],
333
372
code_below = node .attributes ["code_below" ],
334
373
language = cm_language ,
335
374
)
336
375
thebe_source .children = [source ]
337
-
338
- node .children = [thebe_source ]
376
+ input_node .children = [thebe_source ]
339
377
340
378
if not node .attributes ["hide_output" ]:
341
379
thebe_output = ThebeOutputNode ()
342
380
thebe_output .children = output_nodes
343
- if node .attributes ["code_below" ]:
344
- node .children = [thebe_output ] + node .children
345
- else :
346
- node .children = node .children + [thebe_output ]
381
+ output_node += thebe_output
347
382
else :
348
383
if node .attributes ["hide_code" ]:
349
- node .children = []
384
+ node .children . pop ( 0 )
350
385
if not node .attributes ["hide_output" ]:
351
- if node .attributes ["code_below" ]:
352
- node .children = output_nodes + node .children
353
- else :
354
- node .children = node .children + output_nodes
386
+ output_node .children = output_nodes
387
+
388
+ # Now replace the bundle with our OutputNode
389
+ outputbundle_node .replace_self (output_node )
390
+
391
+ # Swap inputs and outputs if we want the code below
392
+ if node .attributes ["code_below" ]:
393
+ node .children = node .children [::- 1 ]
355
394
356
395
357
396
def jupyter_download_role (name , rawtext , text , lineno , inliner ):
@@ -373,3 +412,37 @@ def get_widgets(notebook):
373
412
# Don't catch KeyError, as it's a bug if 'widgets' does
374
413
# not contain 'WIDGET_STATE_MIMETYPE'
375
414
return None
415
+
416
+
417
+ class CellOutputsToNodes (SphinxTransform ):
418
+ """Use the builder context to transform a CellOutputNode into Sphinx nodes."""
419
+
420
+ default_priority = 700
421
+
422
+ def apply (self ):
423
+ thebe_config = self .config .jupyter_sphinx_thebelab_config
424
+
425
+ for cell_node in self .document .traverse (JupyterCellNode ):
426
+ (output_bundle_node ,) = cell_node .traverse (CellOutputBundleNode )
427
+
428
+ # Create doctree nodes for cell outputs.
429
+ output_nodes = cell_output_to_nodes (
430
+ output_bundle_node .outputs ,
431
+ self .config .jupyter_execute_data_priority ,
432
+ bool (cell_node .attributes ["stderr" ]),
433
+ sphinx_abs_dir (self .env ),
434
+ thebe_config ,
435
+ )
436
+ # Remove the outputbundlenode and we'll attach the outputs next
437
+ attach_outputs (output_nodes , cell_node , thebe_config , cell_node .cm_language )
438
+
439
+ # Image collect extra nodes from cell outputs that we need to process
440
+ for node in self .document .traverse (image ):
441
+ # If the image node has `candidates` then it's already been processed
442
+ # as in-line content, so skip it
443
+ if "candidates" in node :
444
+ continue
445
+ # re-initialize an ImageCollector because the `app` imagecollector instance
446
+ # is only available via event listeners.
447
+ col = ImageCollector ()
448
+ col .process_doc (self .app , node )
0 commit comments