| 1 |
# -*- coding: utf-8 -*- |
|---|
| 2 |
# |
|---|
| 3 |
# Copyright (C) 2006 Matthew Good <matt@matt-good.net> |
|---|
| 4 |
# All rights reserved. |
|---|
| 5 |
# |
|---|
| 6 |
# This software is licensed as described in the file COPYING, which |
|---|
| 7 |
# you should have received as part of this distribution. The terms |
|---|
| 8 |
# are also available at http://trac.edgewall.org/wiki/TracLicense. |
|---|
| 9 |
# |
|---|
| 10 |
# Author: Matthew Good <matt@matt-good.net> |
|---|
| 11 |
"""Syntax highlighting based on Pygments.""" |
|---|
| 12 |
|
|---|
| 13 |
from datetime import datetime |
|---|
| 14 |
import os |
|---|
| 15 |
from pkg_resources import resource_filename |
|---|
| 16 |
import re |
|---|
| 17 |
import time |
|---|
| 18 |
|
|---|
| 19 |
from trac.core import * |
|---|
| 20 |
from trac.config import ListOption, Option |
|---|
| 21 |
from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview |
|---|
| 22 |
from trac.wiki.api import IWikiMacroProvider |
|---|
| 23 |
from trac.util.datefmt import http_date |
|---|
| 24 |
from trac.util.html import Markup |
|---|
| 25 |
from trac.web import IRequestHandler, IRequestFilter |
|---|
| 26 |
from trac.web.chrome import add_link, ITemplateProvider |
|---|
| 27 |
|
|---|
| 28 |
try: |
|---|
| 29 |
import pygments |
|---|
| 30 |
from pygments.lexers import get_lexer_by_name |
|---|
| 31 |
from pygments.formatters.html import HtmlFormatter |
|---|
| 32 |
from pygments.styles import get_style_by_name |
|---|
| 33 |
have_pygments = True |
|---|
| 34 |
except ImportError, e: |
|---|
| 35 |
have_pygments = False |
|---|
| 36 |
else: |
|---|
| 37 |
have_pygments = True |
|---|
| 38 |
|
|---|
| 39 |
__all__ = ['PygmentsRenderer'] |
|---|
| 40 |
|
|---|
| 41 |
|
|---|
| 42 |
class PygmentsRenderer(Component): |
|---|
| 43 |
"""Syntax highlighting based on Pygments.""" |
|---|
| 44 |
|
|---|
| 45 |
implements(IHTMLPreviewRenderer, IRequestHandler, IRequestFilter, |
|---|
| 46 |
IWikiMacroProvider, ITemplateProvider) |
|---|
| 47 |
|
|---|
| 48 |
default_style = Option('mimeviewer', 'pygments_default_style', 'trac', |
|---|
| 49 |
"""The default style to use for Pygments syntax highlighting.""") |
|---|
| 50 |
|
|---|
| 51 |
pygments_modes = ListOption('mimeviewer', 'pygments_modes', |
|---|
| 52 |
'', doc= |
|---|
| 53 |
"""List of additional MIME types known by Pygments. |
|---|
| 54 |
|
|---|
| 55 |
For each, a tuple `mimetype:mode:quality` has to be |
|---|
| 56 |
specified, where `mimetype` is the MIME type, |
|---|
| 57 |
`mode` is the corresponding Pygments mode to be used |
|---|
| 58 |
for the conversion and `quality` is the quality ratio |
|---|
| 59 |
associated to this conversion. That can also be used |
|---|
| 60 |
to override the default quality ratio used by the |
|---|
| 61 |
Pygments render.""") |
|---|
| 62 |
|
|---|
| 63 |
expand_tabs = True |
|---|
| 64 |
returns_source = True |
|---|
| 65 |
|
|---|
| 66 |
QUALITY_RATIO = 7 |
|---|
| 67 |
|
|---|
| 68 |
EXAMPLE = """<!DOCTYPE html> |
|---|
| 69 |
<html lang="en"> |
|---|
| 70 |
<head> |
|---|
| 71 |
<title>Hello, world!</title> |
|---|
| 72 |
<script> |
|---|
| 73 |
$(document).ready(function() { |
|---|
| 74 |
$("h1").fadeIn("slow"); |
|---|
| 75 |
}); |
|---|
| 76 |
</script> |
|---|
| 77 |
</head> |
|---|
| 78 |
<body> |
|---|
| 79 |
<h1>Hello, world!</h1> |
|---|
| 80 |
</body> |
|---|
| 81 |
</html>""" |
|---|
| 82 |
|
|---|
| 83 |
def __init__(self): |
|---|
| 84 |
self.log.debug("Pygments installed? %r", have_pygments) |
|---|
| 85 |
if have_pygments: |
|---|
| 86 |
version = getattr(pygments, '__version__', None) |
|---|
| 87 |
if version: |
|---|
| 88 |
self.log.debug('Pygments Version: %s' % version) |
|---|
| 89 |
|
|---|
| 90 |
self._types = None |
|---|
| 91 |
|
|---|
| 92 |
# IHTMLPreviewRenderer implementation |
|---|
| 93 |
|
|---|
| 94 |
def get_quality_ratio(self, mimetype): |
|---|
| 95 |
# Extend default MIME type to mode mappings with configured ones |
|---|
| 96 |
self._init_types() |
|---|
| 97 |
try: |
|---|
| 98 |
return self._types[mimetype][1] |
|---|
| 99 |
except KeyError: |
|---|
| 100 |
return 0 |
|---|
| 101 |
|
|---|
| 102 |
def render(self, req, mimetype, content, filename=None, rev=None): |
|---|
| 103 |
self._init_types() |
|---|
| 104 |
try: |
|---|
| 105 |
mimetype = mimetype.split(';', 1)[0] |
|---|
| 106 |
language = self._types[mimetype][0] |
|---|
| 107 |
return self._highlight(language, content, True) |
|---|
| 108 |
except (KeyError, ValueError): |
|---|
| 109 |
raise Exception("No Pygments lexer found for mime-type '%s'." |
|---|
| 110 |
% mimetype) |
|---|
| 111 |
|
|---|
| 112 |
# IWikiMacroProvider implementation |
|---|
| 113 |
|
|---|
| 114 |
def get_macros(self): |
|---|
| 115 |
self._init_types() |
|---|
| 116 |
return self._languages.keys() |
|---|
| 117 |
|
|---|
| 118 |
def get_macro_description(self, name): |
|---|
| 119 |
self._init_types() |
|---|
| 120 |
return 'Syntax highlighting for %s using Pygments' % self._languages[name] |
|---|
| 121 |
|
|---|
| 122 |
def render_macro(self, req, name, content): |
|---|
| 123 |
self._init_types() |
|---|
| 124 |
return self._highlight(name, content, False) |
|---|
| 125 |
|
|---|
| 126 |
# IRequestFilter |
|---|
| 127 |
|
|---|
| 128 |
def pre_process_request(self, req, handler): |
|---|
| 129 |
return handler |
|---|
| 130 |
|
|---|
| 131 |
def post_process_request(self, req, template, content_type): |
|---|
| 132 |
if not getattr(req, '_no_pygments_stylesheet', False): |
|---|
| 133 |
add_link(req, 'stylesheet', self.env.href('pygments', '%s.css' % |
|---|
| 134 |
req.session.get('pygments_style', self.default_style))) |
|---|
| 135 |
return template, content_type |
|---|
| 136 |
|
|---|
| 137 |
# IRequestHandler implementation |
|---|
| 138 |
|
|---|
| 139 |
def match_request(self, req): |
|---|
| 140 |
if have_pygments: |
|---|
| 141 |
if re.match(r'/pygments/?$', req.path_info): |
|---|
| 142 |
return True |
|---|
| 143 |
match = re.match(r'/pygments/(\w+)\.css$', req.path_info) |
|---|
| 144 |
if match: |
|---|
| 145 |
try: |
|---|
| 146 |
req.args['style'] = get_style_by_name(match.group(1)) |
|---|
| 147 |
except ValueError: |
|---|
| 148 |
return False |
|---|
| 149 |
return True |
|---|
| 150 |
return False |
|---|
| 151 |
|
|---|
| 152 |
def process_request(self, req): |
|---|
| 153 |
# settings panel |
|---|
| 154 |
if not 'style' in req.args: |
|---|
| 155 |
req._no_pygments_stylesheet = True |
|---|
| 156 |
styles = list(get_all_styles()) |
|---|
| 157 |
styles.sort(lambda a, b: cmp(a.lower(), b.lower())) |
|---|
| 158 |
|
|---|
| 159 |
if req.method == 'POST': |
|---|
| 160 |
style = req.args.get('new_style') |
|---|
| 161 |
if style and style in styles: |
|---|
| 162 |
req.session['pygments_style'] = style |
|---|
| 163 |
|
|---|
| 164 |
output = self._highlight('html', self.EXAMPLE, False) |
|---|
| 165 |
req.hdf['output'] = Markup(output) |
|---|
| 166 |
req.hdf['current'] = req.session.get('pygments_style', |
|---|
| 167 |
self.default_style) |
|---|
| 168 |
req.hdf['styles'] = styles |
|---|
| 169 |
req.hdf['pygments_path'] = self.env.href.pygments() |
|---|
| 170 |
return 'pygments_settings.cs', None |
|---|
| 171 |
|
|---|
| 172 |
# provide stylesheet |
|---|
| 173 |
else: |
|---|
| 174 |
style = req.args['style'] |
|---|
| 175 |
|
|---|
| 176 |
parts = style.__module__.split('.') |
|---|
| 177 |
filename = resource_filename('.'.join(parts[:-1]), parts[-1] + '.py') |
|---|
| 178 |
mtime = datetime.utcfromtimestamp(os.path.getmtime(filename)) |
|---|
| 179 |
last_modified = http_date(time.mktime(mtime.timetuple())) |
|---|
| 180 |
if last_modified == req.get_header('If-Modified-Since'): |
|---|
| 181 |
req.send_response(304) |
|---|
| 182 |
req.end_headers() |
|---|
| 183 |
return |
|---|
| 184 |
|
|---|
| 185 |
formatter = HtmlFormatter(style=style) |
|---|
| 186 |
content = u'\n\n'.join([ |
|---|
| 187 |
formatter.get_style_defs('div.code pre'), |
|---|
| 188 |
formatter.get_style_defs('table.code td') |
|---|
| 189 |
]).encode('utf-8') |
|---|
| 190 |
|
|---|
| 191 |
req.send_response(200) |
|---|
| 192 |
req.send_header('Content-Type', 'text/css; charset=utf-8') |
|---|
| 193 |
req.send_header('Last-Modified', last_modified) |
|---|
| 194 |
req.send_header('Content-Length', len(content)) |
|---|
| 195 |
req.write(content) |
|---|
| 196 |
|
|---|
| 197 |
# ITemplateProvider methods |
|---|
| 198 |
|
|---|
| 199 |
def get_templates_dirs(self): |
|---|
| 200 |
return [resource_filename(__name__, 'templates')] |
|---|
| 201 |
|
|---|
| 202 |
def get_htdocs_dirs(self): |
|---|
| 203 |
return () |
|---|
| 204 |
|
|---|
| 205 |
# Internal methods |
|---|
| 206 |
|
|---|
| 207 |
def _init_types(self): |
|---|
| 208 |
if self._types is None: |
|---|
| 209 |
self._types = {} |
|---|
| 210 |
self._languages = {} |
|---|
| 211 |
if have_pygments: |
|---|
| 212 |
for name, aliases, _, mimetypes in get_all_lexers(): |
|---|
| 213 |
for mimetype in mimetypes: |
|---|
| 214 |
self._types[mimetype] = (aliases[0], self.QUALITY_RATIO) |
|---|
| 215 |
for alias in aliases: |
|---|
| 216 |
self._languages[alias] = name |
|---|
| 217 |
self._types.update( |
|---|
| 218 |
Mimeview(self.env).configured_modes_mapping('pygments') |
|---|
| 219 |
) |
|---|
| 220 |
|
|---|
| 221 |
def _highlight(self, language, content, annotate): |
|---|
| 222 |
formatter = HtmlFormatter(cssclass=not annotate and 'code' or '') |
|---|
| 223 |
html = pygments.highlight(content, get_lexer_by_name(language), |
|---|
| 224 |
formatter).rstrip('\n') |
|---|
| 225 |
if annotate: |
|---|
| 226 |
return html[len('<div><pre>'):-len('</pre></div>')].splitlines() |
|---|
| 227 |
return html |
|---|
| 228 |
|
|---|
| 229 |
|
|---|
| 230 |
def get_all_lexers(): |
|---|
| 231 |
from pygments.lexers._mapping import LEXERS |
|---|
| 232 |
from pygments.plugin import find_plugin_lexers |
|---|
| 233 |
|
|---|
| 234 |
for item in LEXERS.itervalues(): |
|---|
| 235 |
yield item[1:] |
|---|
| 236 |
for cls in find_plugin_lexers(): |
|---|
| 237 |
yield cls.name, cls.aliases, cls.filenames, cls.mimetypes |
|---|
| 238 |
|
|---|
| 239 |
|
|---|
| 240 |
def get_all_styles(): |
|---|
| 241 |
from pygments.styles import find_plugin_styles, STYLE_MAP |
|---|
| 242 |
for name in STYLE_MAP: |
|---|
| 243 |
yield name |
|---|
| 244 |
for name, _ in find_plugin_styles(): |
|---|
| 245 |
yield name |
|---|