# -*- coding: utf-8 -*- # # Copyright (C) 2006-2009 Edgewall Software # Copyright (C) 2006 Matthew Good # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.org/wiki/TracLicense. # # Author: Matthew Good """Syntax highlighting based on Pygments.""" from datetime import datetime import os from pkg_resources import resource_filename import re from trac.core import * from trac.config import ListOption, Option from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview from trac.prefs import IPreferencePanelProvider from trac.util import get_module_path, get_pkginfo from trac.util.datefmt import http_date, localtz from trac.web import IRequestHandler from trac.web.chrome import add_stylesheet from genshi import QName, Stream from genshi.core import Attrs, START, END, TEXT # Kludge to workaround the lack of absolute imports in Python version prior to # 2.5 pygments = __import__('pygments', {}, {}, ['lexers', 'styles', 'formatters']) get_all_lexers = pygments.lexers.get_all_lexers get_lexer_by_name = pygments.lexers.get_lexer_by_name HtmlFormatter = pygments.formatters.html.HtmlFormatter get_all_styles = pygments.styles.get_all_styles get_style_by_name = pygments.styles.get_style_by_name __all__ = ['PygmentsRenderer'] class PygmentsRenderer(Component): """Syntax highlighting based on Pygments.""" implements(IHTMLPreviewRenderer, IPreferencePanelProvider, IRequestHandler) 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.""") expand_tabs = True returns_source = True QUALITY_RATIO = 7 EXAMPLE = """ Hello, world!

Hello, world!

""" def __init__(self): version = get_pkginfo(pygments).get('version') # if installed from source, fallback to the hardcoded version info if not version and hasattr(pygments, '__version__'): version = pygments.__version__ self.env.systeminfo.append(('Pygments',version)) self._types = None # IHTMLPreviewRenderer implementation def get_quality_ratio(self, mimetype): # Extend default MIME type to mode mappings with configured ones if self._types is None: self._init_types() try: return self._types[mimetype][1] except KeyError: return 0 def render(self, context, mimetype, content, filename=None, rev=None): req = context.req if self._types is None: self._init_types() add_stylesheet(req, '/pygments/%s.css' % req.session.get('pygments_style', self.default_style)) try: if len(content) > 0: mimetype = mimetype.split(';', 1)[0] 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 def get_preference_panels(self, req): yield ('pygments', 'Pygments Theme') def render_preference_panel(self, req, panel): styles = list(get_all_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: style_cls = get_style_by_name(style) except ValueError, e: raise HTTPNotFound(e) 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 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') 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) # Internal methods def _init_types(self): self._types = {} for lexname, aliases, _, mimetypes in get_all_lexers(): name = aliases and aliases[0] or lexname for mimetype in mimetypes: self._types[mimetype] = (name, self.QUALITY_RATIO) self._types.update( Mimeview(self.env).configured_modes_mapping('pygments') ) def _generate(self, language, content): lexer = get_lexer_by_name(language, stripnl=False) return GenshiHtmlFormatter().generate(lexer.get_tokens(content)) class GenshiHtmlFormatter(HtmlFormatter): """A Pygments formatter subclass that generates a Python stream instead of writing markup as strings to an output file. """ def generate(self, tokens): pos = (None, -1, -1) span = QName('span') class_ = QName('class') def _generate(): attrs = lc = None text = [] for ttype, value in tokens: c = self._get_css_class(ttype) if c == 'n': c = '' if c == lc: text.append(value) elif value: # if no value, leave old span open if text: yield TEXT, u''.join(text), pos if attrs: yield END, span, pos attrs = None text = [value] lc = c if c: attrs = Attrs([(class_, c)]) yield START, (span, attrs), pos if text: yield TEXT, u''.join(text), pos if attrs: yield END, span, pos return Stream(_generate())