Index: tracpygments/__init__.py
===================================================================
--- tracpygments/__init__.py	(revision 1593)
+++ tracpygments/__init__.py	(working copy)
@@ -9,159 +9,213 @@
 #
 # Author: Matthew Good <matt@matt-good.net>
 
-"""Syntax highlighting module, based on the Pygments module.
-"""
+"""Syntax highlighting based on Pygments."""
 
+from datetime import datetime
+import os
+from pkg_resources import resource_filename
 import re
-from StringIO import StringIO
 
 from trac.core import *
-from trac.config import ListOption
+from trac.config import ListOption, Option
 from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview
-from trac.wiki.api import IWikiMacroProvider
+from trac.prefs import IPreferencePanelProvider
+from trac.util.datefmt import http_date, localtz
+from trac.web import IRequestHandler
+from trac.web.chrome import add_stylesheet, ITemplateProvider
 
-__all__ = ['PygmentsRenderer', 'PygmentsProcessors']
+from genshi import QName, Stream
+from genshi.core import Attrs, START, END, TEXT
 
-try:
-    import pygments
-    from pygments.lexers._mapping import LEXERS
-    from pygments.plugin import find_plugin_lexers
-    from pygments.formatters.html import HtmlFormatter
-    from pygments.token import *
-except ImportError:
-    pygments = None
+from pygments import get_lexer_by_name
+from pygments.formatters.html import HtmlFormatter
+from pygments.lexers._mapping import LEXERS
+from pygments.plugin import find_plugin_lexers
+from pygments.styles import find_plugin_styles, get_style_by_name, STYLE_MAP
 
+__all__ = ['PygmentsRenderer']
+
+
 class PygmentsRenderer(Component):
     """Syntax highlighting based on Pygments."""
 
-    implements(IHTMLPreviewRenderer)
+    implements(IHTMLPreviewRenderer, IPreferencePanelProvider, IRequestHandler,
+               ITemplateProvider)
 
+    default_style = Option('mimeviewer', 'pygments_default_style', 'trac',
+        """The default style to use for Pygments syntax highlighting.""")
+
     pygments_modes = ListOption('mimeviewer', 'pygments_modes',
         '', doc=
         """List of additional MIME types known by Pygments.
+        
         For each, a tuple `mimetype:mode:quality` has to be
         specified, where `mimetype` is the MIME type,
         `mode` is the corresponding Pygments mode to be used
         for the conversion and `quality` is the quality ratio
-        associated to this conversion.
-        That can also be used to override the default
-        quality ratio used by the Pygments render.""")
+        associated to this conversion. That can also be used
+        to override the default quality ratio used by the
+        Pygments render.""")
 
     expand_tabs = True
+    returns_source = True
 
-    QUALITY_RATIO = 9
+    QUALITY_RATIO = 7
 
+    EXAMPLE = """<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <title>Hello, world!</title>
+    <script>
+      $(document).ready(function() {
+        $("h1").fadeIn("slow");
+      });
+    </script>
+  </head>
+  <body>
+    <h1>Hello, world!</h1>
+  </body>
+</html>"""
+
     def __init__(self):
         self._types = None
 
+    # IHTMLPreviewRenderer implementation
+
     def get_quality_ratio(self, mimetype):
-        if pygments is None:
-            return 0
         # Extend default MIME type to mode mappings with configured ones
         if self._types is None:
-            self._types = {}
-            for modname, _, aliases, _, mimetypes in _iter_lexerinfo():
-                for mimetype in mimetypes:
-                    self._types[mimetype] = (aliases[0], self.QUALITY_RATIO)
-            self._types.update(
-                Mimeview(self.env).configured_modes_mapping('pygments'))
+            self._init_types()
         try:
             return self._types[mimetype][1]
         except KeyError:
             return 0
 
     def render(self, req, mimetype, content, filename=None, rev=None):
+        if self._types is None:
+            self._init_types()
+        add_stylesheet(req, '/pygments/%s.css' %
+                       req.session.get('pygments_style', self.default_style))
         try:
             mimetype = mimetype.split(';', 1)[0]
-            lang = self._types[mimetype][0]
-            return _format(lang, content, True)
+            language = self._types[mimetype][0]
+            return self._generate(language, content)
         except (KeyError, ValueError):
             raise Exception("No Pygments lexer found for mime-type '%s'."
                             % mimetype)
 
+    # IPreferencePanelProvider implementation
 
-class PygmentsProcessors(Component):
-    implements(IWikiMacroProvider)
+    def get_preference_panels(self, req):
+        yield ('pygments', 'Pygments Theme')
 
-    def __init__(self):
-        self.languages = {}
+    def render_preference_panel(self, req, panel):
+        styles = STYLE_MAP.keys() + [name for name, mod in find_plugin_styles()]
+
+        if req.method == 'POST':
+            style = req.args.get('style')
+            if style and style in styles:
+                req.session['pygments_style'] = style
+            req.redirect(req.href.prefs(panel or None))
+
+        output = self._generate('html', self.EXAMPLE)
+        return 'prefs_pygments.html', {
+            'output': output,
+            'selection': req.session.get('pygments_style', self.default_style),
+            'styles': styles
+        }
+
+    # IRequestHandler implementation
+
+    def match_request(self, req):
+        match = re.match(r'/pygments/(\w+)\.css', req.path_info)
+        if match:
+            req.args['style'] = match.group(1)
+            return True
+
+    def process_request(self, req):
+        style = req.args['style']
         try:
-            for modname, name, aliases, _, _ in _iter_lexerinfo():
-                for alias in aliases:
-                    self.languages[alias] = name
-        except ImportError:
-            pass
+            style_cls = get_style_by_name(style)
+        except ValueError, e:
+            raise HTTPNotFound(e)
 
-    def get_macros(self):
-        return self.languages.keys()
+        parts = style_cls.__module__.split('.')
+        filename = resource_filename('.'.join(parts[:-1]), parts[-1] + '.py')
+        mtime = datetime.fromtimestamp(os.path.getmtime(filename), localtz)
+        last_modified = http_date(mtime)
+        if last_modified == req.get_header('If-Modified-Since'):
+            req.send_response(304)
+            req.end_headers()
+            return
 
-    def get_macro_description(self, name):
-        return 'Syntax highlighting for %s using Pygments' % self.languages[name]
+        formatter = HtmlFormatter(style=style_cls)
+        content = u'\n\n'.join([
+            formatter.get_style_defs('div.code pre'),
+            formatter.get_style_defs('table.code td')
+        ]).encode('utf-8')
 
-    def render_macro(self, req, name, content):
-        return _format(name, content)
+        req.send_response(200)
+        req.send_header('Content-Type', 'text/css; charset=utf-8')
+        req.send_header('Last-Modified', last_modified)
+        req.send_header('Content-Length', len(content))
+        req.write(content)
 
+    # ITemplateProvider implementation
 
+    def get_htdocs_dirs(self):
+        return []
+
+    def get_templates_dirs(self):
+        return [resource_filename(__name__, 'templates')]
+
+    # Internal methods
+
+    def _init_types(self):
+        self._types = {}
+        for modname, _, aliases, _, mimetypes in _iter_lexerinfo():
+            for mimetype in mimetypes:
+                self._types[mimetype] = (aliases[0], self.QUALITY_RATIO)
+        self._types.update(
+            Mimeview(self.env).configured_modes_mapping('pygments')
+        )
+
+    def _generate(self, language, content):
+        lexer = get_lexer_by_name(language)
+        return GenshiHtmlFormatter().generate(lexer.get_tokens(content))
+
+
 def _iter_lexerinfo():
     for info in LEXERS.itervalues():
         yield info
     for cls in find_plugin_lexers():
         yield cls.__module__, cls.name, cls.aliases, cls.filenames, cls.mimetypes
 
-def _format(lang, content, annotate=False):
-    lexer = pygments.get_lexer_by_name(lang)
-    formatter = TracHtmlFormatter(cssclass = not annotate and 'code' or '')
-    html = pygments.highlight(content, lexer, formatter).rstrip('\n')
-    if annotate:
-        return html[len('<div><pre>'):-len('</pre></div>')].splitlines()
-    else:
-        return html
 
-def _issubtoken(token, base):
-    while token is not None:
-        if token == base:
-            return True
-        token = token.parent
-    return False
+class GenshiHtmlFormatter(HtmlFormatter):
 
+    def generate(self, tokens):
+        pos = (None, -1, -1)
+        span = QName('span')
 
-if pygments is not None:
-    class TracHtmlFormatter(HtmlFormatter):
-        # more specific should come before their parents in order to
-        # resolve them in the right order
-        token_classes = [
-            (Comment.Preproc, 'code-prep'),
-            (Comment, 'code-comment'),
-            (Name.Attribute, 'h_attribute'),
-            (Name.Builtin, 'code-lang'),
-            (Name.Class, 'code-type'),
-            #(Name.Constant, 'code-type'),
-            #(Name.Decorator, 'code-type'),
-            (Name.Entity, 'h_entity'),
-            #(Name.Exception, 'code-type'),
-            (Name.Function, 'code-func'),
-            #(Name.Label, 'code-type'),
-            #(Name.Namespace, 'code-type'),
-            (Name.Tag, 'h_tag'),
-            (Name.Variable, 'code-var'),
-            (Operator, 'code-lang'),
-            (String, 'code-string'),
-            # TODO String subtokens
-            (Keyword.Type, 'code-type'),
-            (Keyword, 'code-keyword'),
-        ]
+        def _generate():
+            lattrs = None
 
-        def _get_css_class(self, ttype):
-            try:
-                return self._class_cache[ttype]
-            except KeyError:
-                pass
-            for token, css_class in self.token_classes:
-                if _issubtoken(ttype, token):
-                    break
-            else:
-                css_class = None
-            if css_class is not None:
-                css_class = self.classprefix + css_class
-            self._class_cache[ttype] = css_class
-            return css_class
+            for ttype, value in tokens:
+                attrs = Attrs([('class', self._get_css_class(ttype))])
+
+                if attrs == lattrs:
+                    yield TEXT, value, pos
+
+                elif value: # if no value, leave old span open
+                    if lattrs:
+                        yield END, span, pos
+                    lattrs = attrs
+                    if attrs:
+                        yield START, (span, attrs), pos
+                    yield TEXT, value, pos
+
+            if lattrs:
+                yield END, span, pos
+
+        return Stream(_generate())
Index: tracpygments/templates/prefs_pygments.html
===================================================================
--- tracpygments/templates/prefs_pygments.html	(revision 0)
+++ tracpygments/templates/prefs_pygments.html	(revision 0)
@@ -0,0 +1,47 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/"
+      xmlns:xi="http://www.w3.org/2001/XInclude">
+  <xi:include href="prefs.html" />
+  <head>
+    <title>Pygments Theme</title>
+    <style type="text/css">
+      div.code pre { border: 1px solid #999; font-size: 90%; margin: 1em 2em;
+        padding: 5px; width: 60%;
+      }
+    </style>
+    <link py:for="style in sorted(styles)" rel="stylesheet" type="text/css"
+          href="${href.pygments('%s.css' % style)}" title="${style.title()}" />
+    <script type="text/javascript">
+      function switchStyleSheet(title) {
+        $('link[@rel="stylesheet"][@title]').each(function() {
+          this.disabled = this.getAttribute('title') != title;
+        });
+      }
+      $(document).ready(function() {
+        switchStyleSheet("${selection.title()}");
+        $("#pygment_theme").attr("autocomplete", "off").change(function() {
+          switchStyleSheet(this.options[this.selectedIndex].text);
+        });
+      });
+    </script>
+  </head>
+  <body>
+
+    <div class="field">
+      <p class="hint">The Pygments syntax highlighter can be used with
+      different coloring themes.</p>
+      <p><label>Theme:
+        <select id="pygment_theme" name="style">
+          <option py:for="style in sorted(styles)" value="${style}"
+                  selected="${selection == style or None}">${style.title()}</option>
+        </select>
+      </label></p>
+      Preview:
+      <div class="code"><pre>${output}</pre></div>
+    </div>
+
+  </body>
+</html>
Index: setup.py
===================================================================
--- setup.py	(revision 1593)
+++ setup.py	(working copy)
@@ -23,7 +23,7 @@
     ],
     entry_points={
         'trac.plugins': [
-            'tracpygments = tracpygments',
+            'pygments = tracpygments',
         ],
     }
 )

