All Unkept
Posted in: Python, Blogging and bloggers, Haskell  —  July 19, 2007 at 08:38 PM

reStructuredText, HsColour and Pygments

by Luke Plant

I like to write blog postings in reStructuredText, and I use rst2html from Python's docutils to turn them into HTML before pasting into my blog software.

One thing missing is source highlighting for Haskell, Python etc. Thankfully, Both reST and Python's docutils are written to be extensible. Below is a replacement 'rst2html' which includes support for Haskell colouring using HsColour, and just about everything else using Pygments.

Example usage:

.. code-block:: python

   import os
   # Standard hello world stuff
   class Hello()
       def do_it(self)
           print "Hello world"

   if __name__ == '__main__':
       Hello().do_it()


     def main()
         print "Hello world"

Output:

import os
# Standard hello world stuff
class Hello()
    def do_it(self)
        print "Hello world"

if __name__ == '__main__':
    Hello().do_it()

Some sample Haskell:

class Show a where
    show      :: a -> String
    showsPrec :: Int -> a -> ShowS
    showList  :: [a] -> ShowS

    -- Minimal complete definition: show or showsPrec
    show x          = showsPrec 0 x ""
    showsPrec _ x s = show x ++ s
    showList []     = showString "[]"
    showList (x:xs) = showChar '[' . shows x . showl xs
                      where showl []     = showChar ']'
                            showl (x:xs) = showChar ',' . shows x . showl xs

class Eq a where
    (==), (/=) :: a -> a -> Bool

    -- Minimal complete definition: (==) or (/=)
    x == y      = not (x/=y)
    x /= y      = not (x==y)

Here is the code:

#!/usr/bin/python

"""
rst2html

A minimal front end to the Docutils Publisher, producing HTML,
with an extension for colouring code-blocks
"""

try:
    import locale
    locale.setlocale(locale.LC_ALL, '')
except:
    pass


from docutils import nodes, parsers
from docutils.parsers.rst import states, directives
from docutils.core import publish_cmdline, default_description

import tempfile, os

def getCommandOutput2(command):
    child_stdin, child_stdout = os.popen2(command)
    child_stdin.close()
    data = child_stdout.read()
    err = child_stdout.close()
    if err:
        raise RuntimeError, '%s failed w/ exit code %d' % (command, err)
    return data

def highlight_haskell(text):
    fh, path = tempfile.mkstemp()
    os.write(fh, text)
    output = getCommandOutput2(["HsColour", "-css", "-partial", path])
    os.close(fh)
    return output

def get_highlighter(language):
    if language == 'haskell':
        return highlight_haskell

    from pygments import lexers, util, highlight, formatters
    import StringIO

    try:
        lexer = lexers.get_lexer_by_name(language)
    except util.ClassNotFound:
        return None

    formatter = formatters.get_formatter_by_name('html')
    def _highlighter(code):
        outfile = StringIO.StringIO()
        highlight(code, lexer, formatter, outfile)
        return outfile.getvalue()
    return _highlighter

# Docutils directives:
def code_block(name, arguments, options, content, lineno,
               content_offset, block_text, state, state_machine):
    """
    The code-block directive provides syntax highlighting for blocks
    of code.  It is used with the the following syntax::

    .. code-block:: python

       import sys
       def main():
           sys.stdout.write("Hello world")

    Currently support languages: python (requires pygments),
    haskell (requires HsColour), anything else supported by pygments
    """


    language = arguments[0]
    highlighter = get_highlighter(language)
    if highlighter is None:
        error = state_machine.reporter.error(
            'The "%s" directive does not support language "%s".' % (name, language),
            nodes.literal_block(block_text, block_text), line=lineno)

    if not content:
        error = state_machine.reporter.error(
            'The "%s" block is empty; content required.' % (name),
            nodes.literal_block(block_text, block_text), line=lineno)
        return [error]

    include_text = highlighter("\n".join(content))
    html = '<div class="codeblock %s">\n%s\n</div>\n' % (language, include_text)
    raw = nodes.raw('',html, format='html')
    return [raw]

code_block.arguments = (1,0,0)
code_block.options = {'language' : parsers.rst.directives.unchanged }
code_block.content = 1

# Register
directives.register_directive( 'code-block', code_block )


description = ('Generates (X)HTML documents from standalone reStructuredText '
               'sources.  ' + default_description)

# Command line
publish_cmdline(writer_name='html', description=description)

I borrowed some things from this recipe, thanks. I also discovered Using Pygments in ReST documents after I wrote this.

Comments §

blog comments powered by Disqus