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" :
@@ -270,6 +303,9 @@ def cell_output_to_nodes(cell, data_priority, write_stderr, dir, thebe_config):
270
303
continue
271
304
data = output ["data" ][mime_type ]
272
305
if mime_type .startswith ("image" ):
306
+ ####################################
307
+ # TODO: Figure out how to handle either inline or absolute image paths
308
+
273
309
# Sphinx treats absolute paths as being rooted at the source
274
310
# directory, so make a relative path, which Sphinx treats
275
311
# as being relative to the current working directory.
@@ -326,32 +362,37 @@ def cell_output_to_nodes(cell, data_priority, write_stderr, dir, thebe_config):
326
362
def attach_outputs (output_nodes , node , thebe_config , cm_language ):
327
363
if not node .attributes ["hide_code" ]: # only add css if code is displayed
328
364
node .attributes ["classes" ] = ["jupyter_container" ]
365
+
366
+ input_node = _return_first_node_type (node , CellInputNode )
367
+ outputbundle_node = _return_first_node_type (node , CellOutputBundleNode )
368
+ output_node = CellOutputNode (classes = ["cell_output" ])
329
369
if thebe_config :
330
- source = node .children [0 ]
370
+ # Move the source from the input node into the thebe_source node
371
+ source = input_node .children .pop (0 )
331
372
thebe_source = ThebeSourceNode (
332
373
hide_code = node .attributes ["hide_code" ],
333
374
code_below = node .attributes ["code_below" ],
334
375
language = cm_language ,
335
376
)
336
377
thebe_source .children = [source ]
337
-
338
- node .children = [thebe_source ]
378
+ input_node .children = [thebe_source ]
339
379
340
380
if not node .attributes ["hide_output" ]:
341
381
thebe_output = ThebeOutputNode ()
342
382
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 ]
383
+ output_node += thebe_output
347
384
else :
348
385
if node .attributes ["hide_code" ]:
349
- node .children = []
386
+ node .children . pop ( 0 )
350
387
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
388
+ output_node .children = output_nodes
389
+
390
+ # Now replace the bundle with our OutputNode
391
+ outputbundle_node .replace_self (output_node )
392
+
393
+ # Swap inputs and outputs if we want the code below
394
+ if node .attributes ["code_below" ]:
395
+ node .children = node .children [::- 1 ]
355
396
356
397
357
398
def jupyter_download_role (name , rawtext , text , lineno , inliner ):
@@ -373,3 +414,50 @@ def get_widgets(notebook):
373
414
# Don't catch KeyError, as it's a bug if 'widgets' does
374
415
# not contain 'WIDGET_STATE_MIMETYPE'
375
416
return None
417
+
418
+
419
+ class CellOutputsToNodes (SphinxTransform ):
420
+ """Use the builder context to transform a CellOutputNode into Sphinx nodes."""
421
+
422
+ default_priority = 700
423
+
424
+ def apply (self ):
425
+ thebe_config = self .config .jupyter_sphinx_thebelab_config
426
+
427
+ for cell_node in self .document .traverse (JupyterCellNode ):
428
+ output_bundle_node = _return_first_node_type (
429
+ cell_node , CellOutputBundleNode
430
+ )
431
+ # Create doctree nodes for cell outputs.
432
+ output_nodes = cell_output_to_nodes (
433
+ output_bundle_node .outputs ,
434
+ self .config .jupyter_execute_data_priority ,
435
+ bool (cell_node .attributes ["stderr" ]),
436
+ sphinx_abs_dir (self .env ),
437
+ thebe_config ,
438
+ )
439
+ # Remove the outputbundlenode and we'll attach the outputs next
440
+ attach_outputs (output_nodes , cell_node , thebe_config , cell_node .cm_language )
441
+
442
+ # Image collect extra nodes from cell outputs that we need to process
443
+ for node in self .document .traverse (image ):
444
+ # If the image node has `candidates` then it's already been processed
445
+ # as in-line markdown, so skip it
446
+ if "candidates" in node :
447
+ continue
448
+ col = ImageCollector ()
449
+ col .process_doc (self .app , node )
450
+
451
+
452
+ def _return_first_node_type (node , node_type ):
453
+ found_nodes = list (node .traverse (node_type ))
454
+ if len (found_nodes ) == 0 :
455
+ raise ValueError (f"Found no nodes of type { node_type } in node { node } " )
456
+ if len (found_nodes ) > 1 :
457
+ raise ValueError (
458
+ (
459
+ f"Found more than one nodes of type { node_type } in node { node } . "
460
+ "only return the first instance"
461
+ )
462
+ )
463
+ return found_nodes [0 ]
0 commit comments