A few weeks ago, Fernando Perez, the creator of IPython, wrote a post about blogging with IPython notebooks. I decided to take a stab at making this work in Octopress.

I started by following Fernando's outline: I first went to http://github.com/ipython/nbconvert and obtained the current version of the notebook converter. Running nbconvert.py -f blogger-html filename.ipynb produces a separate html and header file with the notebook content. I inserted the stylesheet info into my header (in octopress, the default location is source/_includes/custom/head.html) and copied the html directly into my post.

I immediately encountered a problem. nbconvert uses global CSS classes and style markups, and some of these (notably the "hightlight" class and the <pre> tag formatting) conflict with styles defined in my octopress theme. The result was that every post in my blog ended up looking like an ugly hybrid of octopress and an ipython notebook. Not very nice.

So I did some surgery. Admittedly, this is a terrible hack, but the following code takes the files output by nbconvert, slices them up, and creates a specific set of CSS classes for the notebook markup, such that there's no longer a conflict with the native octopress styles (you can download this script here):

In [ ]:
#!/usr/bin/python
import os
import sys

try:
    nbconvert = sys.argv[1]
    notebook = sys.argv[2]
except:
    print "usage: python octopress_notebook.py  /path/to/nbconvert.py  /path/to/notebook_file.ipynb"
    sys.exit(-1)

# convert notebook
os.system('%s -f blogger-html %s' % (nbconvert, notebook))

# get out filenames
outfile_root = os.path.splitext(notebook)[0]
body_file = outfile_root + '.html'
header_file = outfile_root + '_header.html'


# read the files
body = open(body_file).read()
header = open(header_file).read()


# replace the highlight tags
body = body.replace('class="highlight"', 'class="highlight-ipynb"')
header = header.replace('highlight', 'highlight-ipynb')


# specify <pre> tags
body = body.replace('<pre', '<pre class="ipynb"')
header = header.replace('html, body', '\n'.join(('pre.ipynb {',
                                                 '  color: black;',
                                                 '  background: #f7f7f7;',
                                                 '  border: 0;',
                                                 '  box-shadow: none;',
                                                 '  margin-bottom: 0;',
                                                 '  padding: 0;'
                                                 '}\n',
                                                 'html, body')))


# create a special div for notebook
body = '<div class="ipynb">\n\n' + body + "\n\n</div>"
header = header.replace('body {', 'div.ipynb {')


# specialize headers
header = header.replace('html, body,',
                        '\n'.join((('h1.ipynb h2.ipynb h3.ipynb '
                                    'h4.ipynb h5.ipynb h6.ipynb {'),
                                   'h1.ipynb h2.ipynb ... {',
                                   '  margin: 0;',
                                   '  padding: 0;',
                                   '  border: 0;',
                                   '  font-size: 100%;',
                                   '  font: inherit;',
                                   '  vertical-align: baseline;',
                                   '}\n',
                                   'html, body,')))
for h in '123456':
    body = body.replace('<h%s' % h, '<h%s class="ipynb"' % h)


# comment out document-level formatting
header = header.replace('html, body,',
                        '/*html, body,*/')
header = header.replace('h1, h2, h3, h4, h5, h6,',
                        '/*h1, h2, h3, h4, h5, h6,*/')

#----------------------------------------------------------------------
# Write the results to file
open(body_file, 'w').write(body)
open(header_file, 'w').write(header)

This code should be run with two arguments: first the file to be converted, then the path to nbconvert.py. Like the native nbconvert.py this produces a separate file of header code (which is inserted once into the master blog header) and body code which can be copied verbatim into the post.

Trying it out

If you haven't noticed already, this post is written entirely in an IPython notebook. So let's see how some things look.

First of all, we can write some math, which is rendered using mathjax:

$f(x) = \int_0^\infty \left(\frac{\sin(x)}{x^2}\right)dx$

As we see, it renders nicely.

Or we can do some inline plotting:

In [1]:
%pylab inline
Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].
For more information, type 'help(pylab)'.
In [2]:
import numpy as np
x = np.linspace(0, 10, 100)
pylab.plot(x, np.sin(x))
Out[2]:
[<matplotlib.lines.Line2D at 0x2cdba90>]

You can do pretty much anything else a notebook does as well. The IPython team did the hard part.

Where to go from here?

There's clearly a cleaner way to do this. If the IPython team would be open to the idea, I think their HTML stylesheets should be modified so that notebooks can be embedded within any CSS framework with as few conflicts as possible. This means getting rid of all top-level formatting in the style-sheets, and removing potentially common class names like "highlight". Once this is done, nbconvert.py could output this directly, obviating the need for my unforgivable hack shown above.

Second, I'd love to build notebook support directly into octopress. If nbconvert.py is available on the user's system, it could be called directly from the Ruby script that generates Octopress HTML. I have about as much experience with Ruby as I do with Swahili (read: None) so this would take some work for me. I'd be happy to pass the baton to any Octopress gurus out there...

Either of those options will smooth out the notebook/blogging combo considerably, and give me the potential to prognosticate Python in perpetuum. By the way, the notebook used to generate this page can be downloaded here. Happy coding!