Ticket #4246: pygments-plugin.diff
| File pygments-plugin.diff, 12.6 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 pkg_resources import resource_filename 15 15 import re 16 from StringIO import StringIO17 16 18 17 from trac.core import * 19 from trac.config import ListOption 18 from trac.config import ListOption, Option 20 19 from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 21 from trac.wiki.api import IWikiMacroProvider 20 from trac.prefs import IPreferencePanelProvider 21 from trac.web import IRequestHandler 22 from trac.web.chrome import add_stylesheet, ITemplateProvider 22 23 23 __all__ = ['PygmentsRenderer', 'PygmentsProcessors'] 24 from genshi import QName, Stream 25 from genshi.core import Attrs, START, END, TEXT 24 26 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 27 from pygments import get_lexer_by_name 28 from pygments.formatters.html import HtmlFormatter 29 from pygments.lexers._mapping import LEXERS 30 from pygments.plugin import find_plugin_lexers 31 from pygments.styles import find_plugin_styles, STYLE_MAP 33 32 33 __all__ = ['PygmentsRenderer'] 34 35 34 36 class PygmentsRenderer(Component): 35 37 """Syntax highlighting based on Pygments.""" 36 38 37 implements(IHTMLPreviewRenderer) 39 implements(IHTMLPreviewRenderer, IPreferencePanelProvider, IRequestHandler, 40 ITemplateProvider) 38 41 42 default_style = Option('mimeviewer', 'pygments_default_style', 'trac', 43 """The default style to use for Pygments syntax highlighting.""") 44 39 45 pygments_modes = ListOption('mimeviewer', 'pygments_modes', 40 46 '', doc= 41 47 """List of additional MIME types known by Pygments. 48 42 49 For each, a tuple `mimetype:mode:quality` has to be 43 50 specified, where `mimetype` is the MIME type, 44 51 `mode` is the corresponding Pygments mode to be used 45 52 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.""")53 associated to this conversion. That can also be used 54 to override the default quality ratio used by the 55 Pygments render.""") 49 56 50 57 expand_tabs = True 51 58 52 QUALITY_RATIO = 959 QUALITY_RATIO = 7 53 60 61 EXAMPLE = """<!DOCTYPE html> 62 <html lang="en"> 63 <head> 64 <title>Hello, world!</title> 65 <script> 66 $(document).ready(function() { 67 $("h1").fadeIn("slow"); 68 }); 69 </script> 70 </head> 71 <body> 72 <h1>Hello, world!</h1> 73 </body> 74 </html>""" 75 54 76 def __init__(self): 55 77 self._types = None 56 78 79 # IHTMLPreviewRenderer implementation 80 57 81 def get_quality_ratio(self, mimetype): 58 if pygments is None:59 return 060 82 # Extend default MIME type to mode mappings with configured ones 61 83 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')) 84 self._init_types() 68 85 try: 69 86 return self._types[mimetype][1] 70 87 except KeyError: 71 88 return 0 72 89 73 90 def render(self, req, mimetype, content, filename=None, rev=None): 91 if self._types is None: 92 self._init_types() 93 add_stylesheet(req, '/pygments/%s.css' % 94 req.session.get('pygments_style', self.default_style)) 74 95 try: 75 96 mimetype = mimetype.split(';', 1)[0] 76 lang = self._types[mimetype][0]77 return _format(lang, content, True)97 language = self._types[mimetype][0] 98 return self._generate(language, content) 78 99 except (KeyError, ValueError): 79 100 raise Exception("No Pygments lexer found for mime-type '%s'." 80 101 % mimetype) 81 102 103 # IPreferencePanelProvider implementation 82 104 83 class PygmentsProcessors(Component):84 implements(IWikiMacroProvider)105 def get_preference_panels(self, req): 106 yield ('pygments', 'Pygments Theme') 85 107 86 def __init__(self): 87 self.languages = {} 88 try: 89 for modname, name, aliases, _, _ in _iter_lexerinfo(): 90 for alias in aliases: 91 self.languages[alias] = name 92 except ImportError: 93 pass 108 def render_preference_panel(self, req, panel): 109 styles = STYLE_MAP.keys() 94 110 95 def get_macros(self): 96 return self.languages.keys() 111 if req.method == 'POST': 112 style = req.args.get('style') 113 if style and style in styles: 114 req.session['pygments_style'] = style 97 115 98 def get_macro_description(self, name): 99 return 'Syntax highlighting for %s using Pygments' % self.languages[name] 116 req.redirect(req.href.prefs(panel or None)) 100 117 101 def render_macro(self, req, name, content): 102 return _format(name, content) 118 output = self._generate('html', self.EXAMPLE) 119 return 'prefs_pygments.html', { 120 'selection': req.session.get('pygments_style', self.default_style), 121 'styles': styles, 'example_output': output 122 } 103 123 124 # IRequestHandler implementation 104 125 126 def match_request(self, req): 127 match = re.match(r'/pygments/(\w+)\.css', req.path_info) 128 if match: 129 req.args['theme'] = match.group(1) 130 return True 131 132 def process_request(self, req): 133 # TODO: caching 134 req.send_response(200) 135 req.send_header('Content-Type', 'text/css; charset=utf-8') 136 137 formatter = HtmlFormatter(style=req.args['theme']) 138 stylesheet = '\n\n'.join([ 139 formatter.get_style_defs('div.code pre'), 140 formatter.get_style_defs('table.code td') 141 ]) 142 req.write(stylesheet) 143 144 # ITemplateProvider implementation 145 146 def get_htdocs_dirs(self): 147 return [] 148 149 def get_templates_dirs(self): 150 return [resource_filename(__name__, 'templates')] 151 152 # Internal methods 153 154 def _init_types(self): 155 self._types = {} 156 for modname, _, aliases, _, mimetypes in _iter_lexerinfo(): 157 for mimetype in mimetypes: 158 self._types[mimetype] = (aliases[0], self.QUALITY_RATIO) 159 self._types.update( 160 Mimeview(self.env).configured_modes_mapping('pygments') 161 ) 162 163 def _generate(self, language, content): 164 lexer = get_lexer_by_name(language) 165 return GenshiHtmlFormatter().generate(lexer.get_tokens(content)) 166 167 105 168 def _iter_lexerinfo(): 106 169 for info in LEXERS.itervalues(): 107 170 yield info 108 171 for cls in find_plugin_lexers(): 109 172 yield cls.__module__, cls.name, cls.aliases, cls.filenames, cls.mimetypes 110 173 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 174 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 175 class GenshiHtmlFormatter(HtmlFormatter): 126 176 177 def generate(self, tokensource): 178 nocls = self.noclasses 179 getcls = self.ttype2class.get 180 c2s = self.class2style 127 181 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 ] 182 pos = (None, -1, -1) 183 span = QName('span') 153 184 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 185 def _generate(): 186 lattrs = None 187 188 for ttype, value in tokensource: 189 if nocls: 190 cclass = getcls(ttype) 191 while cclass is None: 192 ttype = ttype.parent 193 cclass = getcls(ttype) 194 attrs = cclass and Attrs([('style', c2s[cclass][0])]) 195 else: 196 cls = self._get_css_class(ttype) 197 attrs = cls and Attrs([('class', cls)]) 198 199 if attrs == lattrs: 200 yield TEXT, value, pos 201 202 elif value: # if no value, leave old span open 203 if lattrs: 204 yield END, span, pos 205 lattrs = attrs 206 if attrs: 207 yield START, (span, attrs), pos 208 yield TEXT, value, pos 209 210 if lattrs: 211 yield END, span, pos 212 213 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>${example_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 )
