Ticket #4246: pygments-plugin.2.diff
| File pygments-plugin.2.diff, 13.0 KB (added by cmlenz, 2 years ago) |
|---|
-
tracpygments/__init__.py
9 9 # 10 10 # Author: Matthew Good <matt@matt-good.net> 11 11 12 """Syntax highlighting module, based on the Pygments module. 13 """ 12 """Syntax highlighting based on Pygments.""" 14 13 14 from datetime import datetime 15 import os 16 from pkg_resources import resource_filename 15 17 import re 16 from StringIO import StringIO17 18 18 19 from trac.core import * 19 from trac.config import ListOption 20 from trac.config import ListOption, Option 20 21 from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 21 from trac.wiki.api import IWikiMacroProvider 22 from trac.prefs import IPreferencePanelProvider 23 from trac.util.datefmt import http_date, localtz 24 from trac.web import IRequestHandler 25 from trac.web.chrome import add_stylesheet, ITemplateProvider 22 26 23 __all__ = ['PygmentsRenderer', 'PygmentsProcessors'] 27 from genshi import QName, Stream 28 from genshi.core import Attrs, START, END, TEXT 24 29 25 try: 26 import pygments 27 from pygments.lexers._mapping import LEXERS 28 from pygments.plugin import find_plugin_lexers 29 from pygments.formatters.html import HtmlFormatter 30 from pygments.token import * 31 except ImportError: 32 pygments = None 30 from pygments import get_lexer_by_name 31 from pygments.formatters.html import HtmlFormatter 32 from pygments.lexers._mapping import LEXERS 33 from pygments.plugin import find_plugin_lexers 34 from pygments.styles import find_plugin_styles, get_style_by_name, STYLE_MAP 33 35 36 __all__ = ['PygmentsRenderer'] 37 38 34 39 class PygmentsRenderer(Component): 35 40 """Syntax highlighting based on Pygments.""" 36 41 37 implements(IHTMLPreviewRenderer) 42 implements(IHTMLPreviewRenderer, IPreferencePanelProvider, IRequestHandler, 43 ITemplateProvider) 38 44 45 default_style = Option('mimeviewer', 'pygments_default_style', 'trac', 46 """The default style to use for Pygments syntax highlighting.""") 47 39 48 pygments_modes = ListOption('mimeviewer', 'pygments_modes', 40 49 '', doc= 41 50 """List of additional MIME types known by Pygments. 51 42 52 For each, a tuple `mimetype:mode:quality` has to be 43 53 specified, where `mimetype` is the MIME type, 44 54 `mode` is the corresponding Pygments mode to be used 45 55 for the conversion and `quality` is the quality ratio 46 associated to this conversion. 47 That can also be used to override the default48 quality ratio used by thePygments render.""")56 associated to this conversion. That can also be used 57 to override the default quality ratio used by the 58 Pygments render.""") 49 59 50 60 expand_tabs = True 61 returns_source = True 51 62 52 QUALITY_RATIO = 963 QUALITY_RATIO = 7 53 64 65 EXAMPLE = """<!DOCTYPE html> 66 <html lang="en"> 67 <head> 68 <title>Hello, world!</title> 69 <script> 70 $(document).ready(function() { 71 $("h1").fadeIn("slow"); 72 }); 73 </script> 74 </head> 75 <body> 76 <h1>Hello, world!</h1> 77 </body> 78 </html>""" 79 54 80 def __init__(self): 55 81 self._types = None 56 82 83 # IHTMLPreviewRenderer implementation 84 57 85 def get_quality_ratio(self, mimetype): 58 if pygments is None:59 return 060 86 # Extend default MIME type to mode mappings with configured ones 61 87 if self._types is None: 62 self._types = {} 63 for modname, _, aliases, _, mimetypes in _iter_lexerinfo(): 64 for mimetype in mimetypes: 65 self._types[mimetype] = (aliases[0], self.QUALITY_RATIO) 66 self._types.update( 67 Mimeview(self.env).configured_modes_mapping('pygments')) 88 self._init_types() 68 89 try: 69 90 return self._types[mimetype][1] 70 91 except KeyError: 71 92 return 0 72 93 73 94 def render(self, req, mimetype, content, filename=None, rev=None): 95 if self._types is None: 96 self._init_types() 97 add_stylesheet(req, '/pygments/%s.css' % 98 req.session.get('pygments_style', self.default_style)) 74 99 try: 75 100 mimetype = mimetype.split(';', 1)[0] 76 lang = self._types[mimetype][0]77 return _format(lang, content, True)101 language = self._types[mimetype][0] 102 return self._generate(language, content) 78 103 except (KeyError, ValueError): 79 104 raise Exception("No Pygments lexer found for mime-type '%s'." 80 105 % mimetype) 81 106 107 # IPreferencePanelProvider implementation 82 108 83 class PygmentsProcessors(Component):84 implements(IWikiMacroProvider)109 def get_preference_panels(self, req): 110 yield ('pygments', 'Pygments Theme') 85 111 86 def __init__(self): 87 self.languages = {} 112 def render_preference_panel(self, req, panel): 113 styles = STYLE_MAP.keys() + [name for name, mod in find_plugin_styles()] 114 115 if req.method == 'POST': 116 style = req.args.get('style') 117 if style and style in styles: 118 req.session['pygments_style'] = style 119 req.redirect(req.href.prefs(panel or None)) 120 121 output = self._generate('html', self.EXAMPLE) 122 return 'prefs_pygments.html', { 123 'output': output, 124 'selection': req.session.get('pygments_style', self.default_style), 125 'styles': styles 126 } 127 128 # IRequestHandler implementation 129 130 def match_request(self, req): 131 match = re.match(r'/pygments/(\w+)\.css', req.path_info) 132 if match: 133 req.args['style'] = match.group(1) 134 return True 135 136 def process_request(self, req): 137 style = req.args['style'] 88 138 try: 89 for modname, name, aliases, _, _ in _iter_lexerinfo(): 90 for alias in aliases: 91 self.languages[alias] = name 92 except ImportError: 93 pass 139 style_cls = get_style_by_name(style) 140 except ValueError, e: 141 raise HTTPNotFound(e) 94 142 95 def get_macros(self): 96 return self.languages.keys() 143 parts = style_cls.__module__.split('.') 144 filename = resource_filename('.'.join(parts[:-1]), parts[-1] + '.py') 145 mtime = datetime.fromtimestamp(os.path.getmtime(filename), localtz) 146 last_modified = http_date(mtime) 147 if last_modified == req.get_header('If-Modified-Since'): 148 req.send_response(304) 149 req.end_headers() 150 return 97 151 98 def get_macro_description(self, name): 99 return 'Syntax highlighting for %s using Pygments' % self.languages[name] 152 formatter = HtmlFormatter(style=style_cls) 153 content = u'\n\n'.join([ 154 formatter.get_style_defs('div.code pre'), 155 formatter.get_style_defs('table.code td') 156 ]).encode('utf-8') 100 157 101 def render_macro(self, req, name, content): 102 return _format(name, content) 158 req.send_response(200) 159 req.send_header('Content-Type', 'text/css; charset=utf-8') 160 req.send_header('Last-Modified', last_modified) 161 req.send_header('Content-Length', len(content)) 162 req.write(content) 103 163 164 # ITemplateProvider implementation 104 165 166 def get_htdocs_dirs(self): 167 return [] 168 169 def get_templates_dirs(self): 170 return [resource_filename(__name__, 'templates')] 171 172 # Internal methods 173 174 def _init_types(self): 175 self._types = {} 176 for modname, _, aliases, _, mimetypes in _iter_lexerinfo(): 177 for mimetype in mimetypes: 178 self._types[mimetype] = (aliases[0], self.QUALITY_RATIO) 179 self._types.update( 180 Mimeview(self.env).configured_modes_mapping('pygments') 181 ) 182 183 def _generate(self, language, content): 184 lexer = get_lexer_by_name(language) 185 return GenshiHtmlFormatter().generate(lexer.get_tokens(content)) 186 187 105 188 def _iter_lexerinfo(): 106 189 for info in LEXERS.itervalues(): 107 190 yield info 108 191 for cls in find_plugin_lexers(): 109 192 yield cls.__module__, cls.name, cls.aliases, cls.filenames, cls.mimetypes 110 193 111 def _format(lang, content, annotate=False):112 lexer = pygments.get_lexer_by_name(lang)113 formatter = TracHtmlFormatter(cssclass = not annotate and 'code' or '')114 html = pygments.highlight(content, lexer, formatter).rstrip('\n')115 if annotate:116 return html[len('<div><pre>'):-len('</pre></div>')].splitlines()117 else:118 return html119 194 120 def _issubtoken(token, base): 121 while token is not None: 122 if token == base: 123 return True 124 token = token.parent 125 return False 195 class GenshiHtmlFormatter(HtmlFormatter): 126 196 197 def generate(self, tokens): 198 pos = (None, -1, -1) 199 span = QName('span') 127 200 128 if pygments is not None: 129 class TracHtmlFormatter(HtmlFormatter): 130 # more specific should come before their parents in order to 131 # resolve them in the right order 132 token_classes = [ 133 (Comment.Preproc, 'code-prep'), 134 (Comment, 'code-comment'), 135 (Name.Attribute, 'h_attribute'), 136 (Name.Builtin, 'code-lang'), 137 (Name.Class, 'code-type'), 138 #(Name.Constant, 'code-type'), 139 #(Name.Decorator, 'code-type'), 140 (Name.Entity, 'h_entity'), 141 #(Name.Exception, 'code-type'), 142 (Name.Function, 'code-func'), 143 #(Name.Label, 'code-type'), 144 #(Name.Namespace, 'code-type'), 145 (Name.Tag, 'h_tag'), 146 (Name.Variable, 'code-var'), 147 (Operator, 'code-lang'), 148 (String, 'code-string'), 149 # TODO String subtokens 150 (Keyword.Type, 'code-type'), 151 (Keyword, 'code-keyword'), 152 ] 201 def _generate(): 202 lattrs = None 153 203 154 def _get_css_class(self, ttype): 155 try: 156 return self._class_cache[ttype] 157 except KeyError: 158 pass 159 for token, css_class in self.token_classes: 160 if _issubtoken(ttype, token): 161 break 162 else: 163 css_class = None 164 if css_class is not None: 165 css_class = self.classprefix + css_class 166 self._class_cache[ttype] = css_class 167 return css_class 204 for ttype, value in tokens: 205 attrs = Attrs([('class', self._get_css_class(ttype))]) 206 207 if attrs == lattrs: 208 yield TEXT, value, pos 209 210 elif value: # if no value, leave old span open 211 if lattrs: 212 yield END, span, pos 213 lattrs = attrs 214 if attrs: 215 yield START, (span, attrs), pos 216 yield TEXT, value, pos 217 218 if lattrs: 219 yield END, span, pos 220 221 return Stream(_generate()) -
tracpygments/templates/prefs_pygments.html
1 <!DOCTYPE html 2 PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" 3 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 4 <html xmlns="http://www.w3.org/1999/xhtml" 5 xmlns:py="http://genshi.edgewall.org/" 6 xmlns:xi="http://www.w3.org/2001/XInclude"> 7 <xi:include href="prefs.html" /> 8 <head> 9 <title>Pygments Theme</title> 10 <style type="text/css"> 11 div.code pre { border: 1px solid #999; font-size: 90%; margin: 1em 2em; 12 padding: 5px; width: 60%; 13 } 14 </style> 15 <link py:for="style in sorted(styles)" rel="stylesheet" type="text/css" 16 href="${href.pygments('%s.css' % style)}" title="${style.title()}" /> 17 <script type="text/javascript"> 18 function switchStyleSheet(title) { 19 $('link[@rel="stylesheet"][@title]').each(function() { 20 this.disabled = this.getAttribute('title') != title; 21 }); 22 } 23 $(document).ready(function() { 24 switchStyleSheet("${selection.title()}"); 25 $("#pygment_theme").attr("autocomplete", "off").change(function() { 26 switchStyleSheet(this.options[this.selectedIndex].text); 27 }); 28 }); 29 </script> 30 </head> 31 <body> 32 33 <div class="field"> 34 <p class="hint">The Pygments syntax highlighter can be used with 35 different coloring themes.</p> 36 <p><label>Theme: 37 <select id="pygment_theme" name="style"> 38 <option py:for="style in sorted(styles)" value="${style}" 39 selected="${selection == style or None}">${style.title()}</option> 40 </select> 41 </label></p> 42 Preview: 43 <div class="code"><pre>${output}</pre></div> 44 </div> 45 46 </body> 47 </html> -
setup.py
23 23 ], 24 24 entry_points={ 25 25 'trac.plugins': [ 26 ' tracpygments = tracpygments',26 'pygments = tracpygments', 27 27 ], 28 28 } 29 29 )
