55
66import docutils
77from docutils .parsers .rst import Directive , directives
8- from docutils .nodes import math_block
8+ from docutils .nodes import math_block , image
99from sphinx .util import parselinenos
1010from sphinx .addnodes import download_reference
11+ from sphinx .transforms import SphinxTransform
12+ from sphinx .environment .collectors .asset import ImageCollector
1113
1214import ipywidgets .embed
1315import nbconvert
@@ -127,20 +129,24 @@ def run(self):
127129 else :
128130 hl_lines = []
129131
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 ]
144150
145151
146152class JupyterCellNode (docutils .nodes .container ):
@@ -151,6 +157,28 @@ class JupyterCellNode(docutils.nodes.container):
151157 """
152158
153159
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+
154182class JupyterKernelNode (docutils .nodes .Element ):
155183 """Inserted into doctree whenever a JupyterKernel directive is encountered.
156184
@@ -199,12 +227,12 @@ def html(self):
199227 )
200228
201229
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 ):
203231 """Convert a jupyter cell with outputs and filenames to doctree nodes.
204232
205233 Parameters
206234 ----------
207- cell : jupyter cell
235+ outputs : a list of outputs from a Jupyter cell
208236 data_priority : list of mime types
209237 Which media types to prioritize.
210238 write_stderr : bool
@@ -214,9 +242,14 @@ def cell_output_to_nodes(cell, data_priority, write_stderr, dir, thebe_config):
214242 to the source folder prefixed with ``/``.
215243 thebe_config: dict
216244 Thebelab configuration object or None
245+
246+ Returns
247+ -------
248+ to_add : list of docutils nodes
249+ Each output, converted into a docutils node.
217250 """
218251 to_add = []
219- for _ , output in enumerate ( cell . get ( " outputs" , [])) :
252+ for output in outputs :
220253 output_type = output ["output_type" ]
221254 if output_type == "stream" :
222255 if output ["name" ] == "stderr" :
@@ -325,33 +358,39 @@ def cell_output_to_nodes(cell, data_priority, write_stderr, dir, thebe_config):
325358
326359def attach_outputs (output_nodes , node , thebe_config , cm_language ):
327360 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" ])
329367 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 )
331370 thebe_source = ThebeSourceNode (
332371 hide_code = node .attributes ["hide_code" ],
333372 code_below = node .attributes ["code_below" ],
334373 language = cm_language ,
335374 )
336375 thebe_source .children = [source ]
337-
338- node .children = [thebe_source ]
376+ input_node .children = [thebe_source ]
339377
340378 if not node .attributes ["hide_output" ]:
341379 thebe_output = ThebeOutputNode ()
342380 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
347382 else :
348383 if node .attributes ["hide_code" ]:
349- node .children = []
384+ node .children . pop ( 0 )
350385 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 ]
355394
356395
357396def jupyter_download_role (name , rawtext , text , lineno , inliner ):
@@ -373,3 +412,37 @@ def get_widgets(notebook):
373412 # Don't catch KeyError, as it's a bug if 'widgets' does
374413 # not contain 'WIDGET_STATE_MIMETYPE'
375414 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