Edgewall Software

Ticket #4246: pygments-plugin.2.diff

File pygments-plugin.2.diff, 13.0 KB (added by cmlenz, 2 years ago)

Updated patch for Pygments plugin

  • tracpygments/__init__.py

     
    99# 
    1010# Author: Matthew Good <matt@matt-good.net> 
    1111 
    12 """Syntax highlighting module, based on the Pygments module. 
    13 """ 
     12"""Syntax highlighting based on Pygments.""" 
    1413 
     14from datetime import datetime 
     15import os 
     16from pkg_resources import resource_filename 
    1517import re 
    16 from StringIO import StringIO 
    1718 
    1819from trac.core import * 
    19 from trac.config import ListOption 
     20from trac.config import ListOption, Option 
    2021from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 
    21 from trac.wiki.api import IWikiMacroProvider 
     22from trac.prefs import IPreferencePanelProvider 
     23from trac.util.datefmt import http_date, localtz 
     24from trac.web import IRequestHandler 
     25from trac.web.chrome import add_stylesheet, ITemplateProvider 
    2226 
    23 __all__ = ['PygmentsRenderer', 'PygmentsProcessors'] 
     27from genshi import QName, Stream 
     28from genshi.core import Attrs, START, END, TEXT 
    2429 
    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 
     30from pygments import get_lexer_by_name 
     31from pygments.formatters.html import HtmlFormatter 
     32from pygments.lexers._mapping import LEXERS 
     33from pygments.plugin import find_plugin_lexers 
     34from pygments.styles import find_plugin_styles, get_style_by_name, STYLE_MAP 
    3335 
     36__all__ = ['PygmentsRenderer'] 
     37 
     38 
    3439class PygmentsRenderer(Component): 
    3540    """Syntax highlighting based on Pygments.""" 
    3641 
    37     implements(IHTMLPreviewRenderer) 
     42    implements(IHTMLPreviewRenderer, IPreferencePanelProvider, IRequestHandler, 
     43               ITemplateProvider) 
    3844 
     45    default_style = Option('mimeviewer', 'pygments_default_style', 'trac', 
     46        """The default style to use for Pygments syntax highlighting.""") 
     47 
    3948    pygments_modes = ListOption('mimeviewer', 'pygments_modes', 
    4049        '', doc= 
    4150        """List of additional MIME types known by Pygments. 
     51         
    4252        For each, a tuple `mimetype:mode:quality` has to be 
    4353        specified, where `mimetype` is the MIME type, 
    4454        `mode` is the corresponding Pygments mode to be used 
    4555        for the conversion and `quality` is the quality ratio 
    46         associated to this conversion. 
    47         That can also be used to override the default 
    48         quality ratio used by the Pygments render.""") 
     56        associated to this conversion. That can also be used 
     57        to override the default quality ratio used by the 
     58        Pygments render.""") 
    4959 
    5060    expand_tabs = True 
     61    returns_source = True 
    5162 
    52     QUALITY_RATIO = 9 
     63    QUALITY_RATIO = 7 
    5364 
     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 
    5480    def __init__(self): 
    5581        self._types = None 
    5682 
     83    # IHTMLPreviewRenderer implementation 
     84 
    5785    def get_quality_ratio(self, mimetype): 
    58         if pygments is None: 
    59             return 0 
    6086        # Extend default MIME type to mode mappings with configured ones 
    6187        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() 
    6889        try: 
    6990            return self._types[mimetype][1] 
    7091        except KeyError: 
    7192            return 0 
    7293 
    7394    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)) 
    7499        try: 
    75100            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) 
    78103        except (KeyError, ValueError): 
    79104            raise Exception("No Pygments lexer found for mime-type '%s'." 
    80105                            % mimetype) 
    81106 
     107    # IPreferencePanelProvider implementation 
    82108 
    83 class PygmentsProcessors(Component): 
    84     implements(IWikiMacroProvider) 
     109    def get_preference_panels(self, req): 
     110        yield ('pygments', 'Pygments Theme') 
    85111 
    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'] 
    88138        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) 
    94142 
    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 
    97151 
    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') 
    100157 
    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) 
    103163 
     164    # ITemplateProvider implementation 
    104165 
     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 
    105188def _iter_lexerinfo(): 
    106189    for info in LEXERS.itervalues(): 
    107190        yield info 
    108191    for cls in find_plugin_lexers(): 
    109192        yield cls.__module__, cls.name, cls.aliases, cls.filenames, cls.mimetypes 
    110193 
    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 html 
    119194 
    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 
     195class GenshiHtmlFormatter(HtmlFormatter): 
    126196 
     197    def generate(self, tokens): 
     198        pos = (None, -1, -1) 
     199        span = QName('span') 
    127200 
    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 
    153203 
    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

     
    2323    ], 
    2424    entry_points={ 
    2525        'trac.plugins': [ 
    26             'tracpygments = tracpygments', 
     26            'pygments = tracpygments', 
    2727        ], 
    2828    } 
    2929)