0001"""Python source code colorizer.
0002
0003This module is derived from MoinMoin's [1] python source parser, described
0004in the following recipe:
0005
0006<http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52298>
0007
0008.. [1] http://moin.sourceforge.net/
0009
0010"""
0011
0012# Imports
0013import cgi, string, sys, cStringIO
0014import keyword, token, tokenize
0015import re
0016
0017#############################################################################
0018### Python Source Parser (does Hilighting)
0019#############################################################################
0020
0021_KEYWORD = token.NT_OFFSET + 1
0022_TEXT    = token.NT_OFFSET + 2
0023
0024_styles = {
0025    token.NUMBER:       'number',
0026    token.OP:           'op',
0027    token.STRING:       'string',
0028    tokenize.COMMENT:   'comment',
0029    token.NAME:         'name',
0030    token.ERRORTOKEN:   'error',
0031    _KEYWORD:           'keyword',
0032    _TEXT:              'text',
0033}
0034
0035class Parser:
0036    """ Send colored python source."""
0037
0038    def __init__(self, filename, out = sys.stdout):
0039        """ Store the source text.
0040        """
0041        self.filename = filename
0042        self.raw = open(filename, 'r').read().expandtabs().strip()
0043        self.out = out
0044
0045    def format(self):
0046        """ Parse and send the colored source."""
0047        # store line offsets in self.lines
0048        self.lines = [0, 0]
0049        pos = 0
0050        while 1:
0051            pos = self.raw.find('\n', pos) + 1
0052            if not pos: break
0053            self.lines.append(pos)
0054        self.lines.append(len(self.raw))
0055
0056        # parse the source and write it
0057        self.out.write('''<html><head><title>%s</title><style>
0058        div.python {
0059          color: #333
0060        }
0061        div.python a.lnum {
0062          color: #555;
0063          background-color: #eee;
0064          border-right: 1px solid #999;
0065          padding-right: 2px;
0066          margin-right: 4px;
0067        }
0068        div.python span.comment { color: #933 }
0069        div.python span.keyword { color: #a3e; font-weight: bold  }
0070        div.python span.op { color: #c96 }
0071        div.python span.string { color: #6a6 }
0072        div.python span.name { }
0073        div.python span.text { color: #333 }
0074
0075        </style></head><body>''' % cgi.escape(self.filename))
0076        self.out.write('<div class="python"><code>')
0077        self.write_line(1, br='')
0078        self.pos = 0
0079        text = cStringIO.StringIO(self.raw)
0080        self.run_tokens(tokenize.generate_tokens(text.readline))
0081        self.out.write('</code></div>')
0082        self.out.write('</body></html>')
0083        self.out.flush()
0084        self.out.close()
0085
0086    def write_line(self, line_num, br='<br />\n'):
0087        fmt = str(line_num).rjust(4,'0')
0088        self.out.write(
0089                '%s<a class="lnum" href="#%d" name="%d">%s</a>'                   % (br, line_num, line_num, fmt))
0091
0092    def run_tokens(self, it):
0093        """ Token handler."""
0094        for tok in it:
0095            (toktype, toktext, (srow,scol), (erow,ecol), line) = tok
0096            if 0:
0097                print repr((toktype, token.tok_name[toktype], toktext,
0098                            srow,scol, erow,ecol))
0099
0100            # calculate new positions
0101            oldpos = self.pos
0102            newpos = self.lines[srow] + scol
0103            self.pos = newpos + len(toktext)
0104
0105            # handle newlines
0106            if toktype in [token.NEWLINE, tokenize.NL]:
0107                self.write_line(srow+1)
0108                continue
0109
0110            # send the original whitespace, if needed
0111            if newpos > oldpos:
0112                ws = self.raw[oldpos:newpos]
0113                self.out.write('&#0160;' * len(ws))
0114
0115            # skip indenting tokens
0116            if toktype in [token.INDENT, token.DEDENT]:
0117                self.pos = newpos
0118                continue
0119
0120            # map token type to a color group
0121            if token.LPAR <= toktype and toktype <= token.OP:
0122                toktype = token.OP
0123            elif toktype == token.NAME and keyword.iskeyword(toktext):
0124                toktype = _KEYWORD
0125            style = _styles.get(toktype, _styles[_TEXT])
0126
0127            # send text
0128            self.runlines(toktext, srow, '<span class="%s">%%s</span>' % style)
0129
0130
0131    def runlines(self, text, line_num, interpolate):
0132        if '\n' in text:
0133            lines = text.split('\n')
0134            for i, line in zip(range(len(lines)), lines):
0135                if i > 0: self.write_line(line_num + i)
0136                self.out.write(interpolate % cgi.escape(line).replace(' ', '&#0160;'))
0137        elif text:
0138            self.out.write(interpolate % cgi.escape(text).replace(' ', '&#0160;'))
0139
0140__all__ = ['Parser']
0141
0142# module attributes
0143__author__ = "Ryan Tomayko <rtomayko@gmail.com>"
0144__date__ = "$Date: 2005-05-28 04:58:44 -0400 (Sat, 28 May 2005) $"
0145__revision__ = "$Revision: 12 $"
0146__url__ = "$URL: svn://lesscode.org/pudge/trunk/pudge/colorizer.py $"
0147__copyright__ = "Copyright 2005, Ryan Tomayko"
0148__license__ = "MIT <http://www.opensource.org/licenses/mit-license.php>"