#!/usr/bin/env python """A really simple notebook to rst/html exporter. Usage ./nb2html.py file.ipynb Produces 'file.rst' and 'file.html', along with auto-generated figure files called nb_figure_NN.png. """ import os import subprocess import sys from IPython.external import argparse from IPython.nbformat import current as nbformat from IPython.utils.text import indent from decorators import DocInherit # Cell converters def unknown_cell(cell): """Default converter for cells of unknown type. """ return rst_directive('.. warning:: Unknown cell') + \ [repr(cell)] def rst_directive(directive, text=''): out = [directive, ''] if text: out.extend([indent(text), '']) return out # Converters for parts of a cell. class ConversionException(Exception): pass class Converter(object): default_encoding = 'utf-8' def __init__(self, infile): self.infile = infile self.dirpath = os.path.dirname(infile) @property def extension(self): raise ConversionException("""extension must be defined in Converter subclass""") def dispatch(self, cell_type): """return cell_type dependent render method, for example render_code """ # XXX: unknown_cell here is RST specific - make it generic return getattr(self, 'render_' + cell_type, unknown_cell) def convert(self): lines = [] lines.extend(self.optional_header()) for cell in self.nb.worksheets[0].cells: conv_fn = self.dispatch(cell.cell_type) lines.extend(conv_fn(cell)) lines.append('') lines.extend(self.optional_footer()) return '\n'.join(lines) def render(self): "read, convert, and save self.infile" self.read() self.output = self.convert() return self.save() def read(self): "read and parse notebook into NotebookNode called self.nb" with open(self.infile) as f: self.nb = nbformat.read(f, 'json') def save(self, infile=None, encoding=None): "read and parse notebook into self.nb" if infile is None: infile = os.path.splitext(self.infile)[0] + '.' + self.extension if encoding is None: encoding = self.default_encoding with open(infile, 'w') as f: f.write(self.output.encode(encoding)) return infile def optional_header(): pass def optional_footer(): pass def render_heading(self, cell): """convert a heading cell Returns list.""" raise NotImplementedError def render_code(self, cell): """Convert a code cell Returns list.""" raise NotImplementedError def render_markdown(self, cell): """convert a markdown cell Returns list.""" raise NotImplementedError def render_pyout(self, cell): """convert pyout part of a code cell Returns list.""" raise NotImplementedError def render_display_data(self, cell): """convert display data from the output of a code cell Returns list. """ raise NotImplementedError def render_stream(self, cell): """convert stream part of a code cell Returns list.""" raise NotImplementedError def render_plaintext(self, cell): """convert plain text Returns list.""" raise NotImplementedError class ConverterRST(Converter): extension = 'rst' figures_counter = 0 heading_level = {1: '=', 2: '-', 3: '`', 4: '\'', 5: '.', 6: '~'} @DocInherit def render_heading(self, cell): marker = self.heading_level[cell.level] return ['{0}\n{1}\n'.format(cell.source, marker * len(cell.source))] @DocInherit def render_code(self, cell): if not cell.input: return [] lines = ['In[%s]:' % cell.prompt_number, ''] lines.extend(rst_directive('.. code:: python', cell.input)) for output in cell.outputs: conv_fn = self.dispatch(output.output_type) lines.extend(conv_fn(output)) return lines @DocInherit def render_markdown(self, cell): return [cell.source] @DocInherit def render_plaintext(self, cell): return [cell.source] @DocInherit def render_pyout(self, output): lines = ['Out[%s]:' % output.prompt_number, ''] # output is a dictionary like object with type as a key if 'latex' in output: lines.extend(rst_directive('.. math::', output.latex)) if 'text' in output: lines.extend(rst_directive('.. parsed-literal::', output.text)) return lines @DocInherit def render_display_data(self, output): lines = [] if 'png' in output: infile = 'nb_figure_%s.png' % self.figures_counter fullname = os.path.join(self.dirpath, infile) with open(fullname, 'w') as f: f.write(output.png.decode('base64')) self.figures_counter += 1 lines.append('.. image:: %s' % infile) lines.append('') return lines @DocInherit def render_stream(self, output): lines = [] if 'text' in output: lines.extend(rst_directive('.. parsed-literal::', output.text)) return lines class ConverterQuickHTML(Converter): extension = 'html' figures_counter = 0 def optional_header(self): # XXX: inject the IPython standard CSS into here s = """
""" return s.splitlines() def optional_footer(self): s = """ """ return s.splitlines() @DocInherit def render_heading(self, cell): marker = cell.level return ['In [%s]: | ' % cell.prompt_number)
lines.append(" \n".join(cell.input.splitlines())) lines.append(' |
') conv_fn = self.dispatch(output.output_type) lines.extend(conv_fn(output)) lines.append(' |
"+cell.source+""] @DocInherit def render_plaintext(self, cell): return ["
"+cell.source+""] @DocInherit def render_pyout(self, output): lines = ['
") lines.extend(indent(output.latex)) lines.append("") if 'text' in output: lines.append("
") lines.extend(indent(output.text)) lines.append("") return lines @DocInherit def render_display_data(self, output): lines = [] if 'png' in output: infile = 'nb_figure_%s.png' % self.figures_counter fullname = os.path.join(self.dirpath, infile) with open(fullname, 'w') as f: f.write(output.png.decode('base64')) self.figures_counter += 1 lines.append('