Package trac :: Package mimeview :: Module pygments

Source Code for Module trac.mimeview.pygments

  1  # -*- coding: utf-8 -*- 
  2  # 
  3  # Copyright (C) 2006-2020 Edgewall Software 
  4  # Copyright (C) 2006 Matthew Good <[email protected]> 
  5  # All rights reserved. 
  6  # 
  7  # This software is licensed as described in the file COPYING, which 
  8  # you should have received as part of this distribution. The terms 
  9  # are also available at https://trac.edgewall.org/wiki/TracLicense. 
 10  # 
 11  # Author: Matthew Good <[email protected]> 
 12   
 13  from __future__ import absolute_import 
 14   
 15  import os 
 16  import pygments 
 17  import re 
 18  from datetime import datetime 
 19  from pkg_resources import resource_filename 
 20  from pygments.formatters.html import HtmlFormatter 
 21  from pygments.lexers import get_all_lexers, get_lexer_by_name 
 22  from pygments.styles import get_all_styles, get_style_by_name 
 23   
 24  from trac.core import * 
 25  from trac.config import ConfigSection, ListOption, Option 
 26  from trac.env import ISystemInfoProvider 
 27  from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 
 28  from trac.prefs import IPreferencePanelProvider 
 29  from trac.util import get_pkginfo, lazy 
 30  from trac.util.datefmt import http_date, localtz 
 31  from trac.util.translation import _ 
 32  from trac.web.api import IRequestHandler, HTTPNotFound 
 33  from trac.web.chrome import ITemplateProvider, add_notice, add_stylesheet 
 34   
 35  from genshi import QName, Stream 
 36  from genshi.core import Attrs, START, END, TEXT 
 37   
 38  __all__ = ['PygmentsRenderer'] 
39 40 41 -class PygmentsRenderer(Component):
42 """HTML renderer for syntax highlighting based on Pygments.""" 43 44 implements(ISystemInfoProvider, IHTMLPreviewRenderer, 45 IPreferencePanelProvider, IRequestHandler, 46 ITemplateProvider) 47 48 is_valid_default_handler = False 49 50 pygments_lexer_options = ConfigSection('pygments-lexer', 51 """Configure Pygments [%(url)s lexer] options. 52 53 For example, to set the 54 [%(url)s#lexers-for-php-and-related-languages PhpLexer] options 55 `startinline` and `funcnamehighlighting`: 56 {{{#!ini 57 [pygments-lexer] 58 php.startinline = True 59 php.funcnamehighlighting = True 60 }}} 61 62 The lexer name is derived from the class name, with `Lexer` stripped 63 from the end. The lexer //short names// can also be used in place 64 of the lexer name. 65 """, doc_args={'url': 'http://pygments.org/docs/lexers/'}) 66 67 default_style = Option('mimeviewer', 'pygments_default_style', 'trac', 68 """The default style to use for Pygments syntax highlighting.""") 69 70 pygments_modes = ListOption('mimeviewer', 'pygments_modes', 71 '', doc= 72 """List of additional MIME types known by Pygments. 73 74 For each, a tuple `mimetype:mode:quality` has to be 75 specified, where `mimetype` is the MIME type, 76 `mode` is the corresponding Pygments mode to be used 77 for the conversion and `quality` is the quality ratio 78 associated to this conversion. That can also be used 79 to override the default quality ratio used by the 80 Pygments render.""") 81 82 expand_tabs = True 83 returns_source = True 84 85 QUALITY_RATIO = 7 86 87 EXAMPLE = """<!DOCTYPE html> 88 <html lang="en"> 89 <head> 90 <title>Hello, world!</title> 91 <script> 92 jQuery(document).ready(function($) { 93 $("h1").fadeIn("slow"); 94 }); 95 </script> 96 </head> 97 <body> 98 <h1>Hello, world!</h1> 99 </body> 100 </html>""" 101 102 # ISystemInfoProvider methods 103
104 - def get_system_info(self):
105 version = get_pkginfo(pygments).get('version') 106 # if installed from source, fallback to the hardcoded version info 107 if not version and hasattr(pygments, '__version__'): 108 version = pygments.__version__ 109 yield 'Pygments', version
110 111 # IHTMLPreviewRenderer methods 112
113 - def get_extra_mimetypes(self):
114 for _, aliases, _, mimetypes in get_all_lexers(): 115 for mimetype in mimetypes: 116 yield mimetype, aliases
117
118 - def get_quality_ratio(self, mimetype):
119 # Extend default MIME type to mode mappings with configured ones 120 try: 121 return self._types[mimetype][1] 122 except KeyError: 123 return 0
124
125 - def render(self, context, mimetype, content, filename=None, rev=None):
126 req = context.req 127 style = req.session.get('pygments_style', self.default_style) 128 add_stylesheet(req, '/pygments/%s.css' % style) 129 try: 130 if len(content) > 0: 131 mimetype = mimetype.split(';', 1)[0] 132 language = self._types[mimetype][0] 133 return self._generate(language, content, context) 134 except (KeyError, ValueError): 135 raise Exception("No Pygments lexer found for mime-type '%s'." 136 % mimetype)
137 138 # IPreferencePanelProvider methods 139
140 - def get_preference_panels(self, req):
141 yield 'pygments', _('Syntax Highlighting')
142
143 - def render_preference_panel(self, req, panel):
144 styles = list(get_all_styles()) 145 146 if req.method == 'POST': 147 style = req.args.get('style') 148 if style and style in styles: 149 req.session['pygments_style'] = style 150 add_notice(req, _("Your preferences have been saved.")) 151 req.redirect(req.href.prefs(panel or None)) 152 153 for style in sorted(styles): 154 add_stylesheet(req, '/pygments/%s.css' % style, title=style.title()) 155 output = self._generate('html', self.EXAMPLE) 156 return 'prefs_pygments.html', { 157 'output': output, 158 'selection': req.session.get('pygments_style', self.default_style), 159 'styles': styles 160 }
161 162 # IRequestHandler methods 163
164 - def match_request(self, req):
165 match = re.match(r'/pygments/([-\w]+)\.css', req.path_info) 166 if match: 167 req.args['style'] = match.group(1) 168 return True
169
170 - def process_request(self, req):
171 style = req.args['style'] 172 try: 173 style_cls = get_style_by_name(style) 174 except ValueError as e: 175 raise HTTPNotFound(e) 176 177 parts = style_cls.__module__.split('.') 178 filename = resource_filename('.'.join(parts[:-1]), parts[-1] + '.py') 179 mtime = datetime.fromtimestamp(os.path.getmtime(filename), localtz) 180 last_modified = http_date(mtime) 181 if last_modified == req.get_header('If-Modified-Since'): 182 req.send_response(304) 183 req.end_headers() 184 return 185 186 formatter = HtmlFormatter(style=style_cls) 187 content = u'\n\n'.join([ 188 formatter.get_style_defs('div.code pre'), 189 formatter.get_style_defs('table.code td') 190 ]).encode('utf-8') 191 192 req.send_response(200) 193 req.send_header('Content-Type', 'text/css; charset=utf-8') 194 req.send_header('Last-Modified', last_modified) 195 req.send_header('Content-Length', len(content)) 196 req.write(content)
197 198 # ITemplateProvider methods 199
200 - def get_htdocs_dirs(self):
201 return []
202
203 - def get_templates_dirs(self):
204 return [resource_filename('trac.mimeview', 'templates')]
205 206 # Internal methods 207 208 @lazy
209 - def _lexer_alias_name_map(self):
210 lexer_alias_name_map = {} 211 for lexer_name, aliases, _, _ in get_all_lexers(): 212 name = aliases[0] if aliases else lexer_name 213 for alias in aliases: 214 lexer_alias_name_map[alias] = name 215 return lexer_alias_name_map
216 217 @lazy
218 - def _lexer_options(self):
219 lexer_options = {} 220 for key, lexer_option_value in self.pygments_lexer_options.options(): 221 try: 222 lexer_name_or_alias, lexer_option_name = key.split('.') 223 except ValueError: 224 pass 225 else: 226 lexer_name = self._lexer_alias_to_name(lexer_name_or_alias) 227 lexer_option = {lexer_option_name: lexer_option_value} 228 lexer_options.setdefault(lexer_name, {}).update(lexer_option) 229 return lexer_options
230 231 @lazy
232 - def _types(self):
233 types = {} 234 for lexer_name, aliases, _, mimetypes in get_all_lexers(): 235 name = aliases[0] if aliases else lexer_name 236 for mimetype in mimetypes: 237 types[mimetype] = (name, self.QUALITY_RATIO) 238 239 # Pygments < 1.4 doesn't know application/javascript 240 if 'application/javascript' not in types: 241 js_entry = types.get('text/javascript') 242 if js_entry: 243 types['application/javascript'] = js_entry 244 245 types.update(Mimeview(self.env).configured_modes_mapping('pygments')) 246 return types
247
248 - def _generate(self, language, content, context=None):
249 lexer_name = self._lexer_alias_to_name(language) 250 lexer_options = {'stripnl': False} 251 lexer_options.update(self._lexer_options.get(lexer_name, {})) 252 if context: 253 lexer_options.update(context.get_hint('lexer_options', {})) 254 lexer = get_lexer_by_name(lexer_name, **lexer_options) 255 return GenshiHtmlFormatter().generate(lexer.get_tokens(content))
256
257 - def _lexer_alias_to_name(self, alias):
258 return self._lexer_alias_name_map.get(alias, alias)
259
260 261 -class GenshiHtmlFormatter(HtmlFormatter):
262 """A Pygments formatter subclass that generates a Python stream instead 263 of writing markup as strings to an output file. 264 """ 265
266 - def _chunk(self, tokens):
267 """Groups tokens with the same CSS class in the token stream 268 and yields them one by one, along with the CSS class, with the 269 values chunked together.""" 270 271 last_class = None 272 text = [] 273 for ttype, value in tokens: 274 c = self._get_css_class(ttype) 275 if c == 'n': 276 c = '' 277 if c == last_class: 278 text.append(value) 279 continue 280 281 # If no value, leave the old <span> open. 282 if value: 283 yield last_class, u''.join(text) 284 text = [value] 285 last_class = c 286 287 if text: 288 yield last_class, u''.join(text)
289
290 - def generate(self, tokens):
291 pos = None, -1, -1 292 span = QName('span') 293 class_ = QName('class') 294 295 def _generate(): 296 for c, text in self._chunk(tokens): 297 if c: 298 attrs = Attrs([(class_, c)]) 299 yield START, (span, attrs), pos 300 yield TEXT, text, pos 301 yield END, span, pos 302 else: 303 yield TEXT, text, pos
304 return Stream(_generate())
305