Edgewall Software

Ticket #4246: pygments-plugin.diff

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

Patch for TracPygments 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 pkg_resources import resource_filename 
    1515import re 
    16 from StringIO import StringIO 
    1716 
    1817from trac.core import * 
    19 from trac.config import ListOption 
     18from trac.config import ListOption, Option 
    2019from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 
    21 from trac.wiki.api import IWikiMacroProvider 
     20from trac.prefs import IPreferencePanelProvider 
     21from trac.web import IRequestHandler 
     22from trac.web.chrome import add_stylesheet, ITemplateProvider 
    2223 
    23 __all__ = ['PygmentsRenderer', 'PygmentsProcessors'] 
     24from genshi import QName, Stream 
     25from genshi.core import Attrs, START, END, TEXT 
    2426 
    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 
     27from pygments import get_lexer_by_name 
     28from pygments.formatters.html import HtmlFormatter 
     29from pygments.lexers._mapping import LEXERS 
     30from pygments.plugin import find_plugin_lexers 
     31from pygments.styles import find_plugin_styles, STYLE_MAP 
    3332 
     33__all__ = ['PygmentsRenderer'] 
     34 
     35 
    3436class PygmentsRenderer(Component): 
    3537    """Syntax highlighting based on Pygments.""" 
    3638 
    37     implements(IHTMLPreviewRenderer) 
     39    implements(IHTMLPreviewRenderer, IPreferencePanelProvider, IRequestHandler, 
     40               ITemplateProvider) 
    3841 
     42    default_style = Option('mimeviewer', 'pygments_default_style', 'trac', 
     43        """The default style to use for Pygments syntax highlighting.""") 
     44 
    3945    pygments_modes = ListOption('mimeviewer', 'pygments_modes', 
    4046        '', doc= 
    4147        """List of additional MIME types known by Pygments. 
     48         
    4249        For each, a tuple `mimetype:mode:quality` has to be 
    4350        specified, where `mimetype` is the MIME type, 
    4451        `mode` is the corresponding Pygments mode to be used 
    4552        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.""") 
     53        associated to this conversion. That can also be used 
     54        to override the default quality ratio used by the 
     55        Pygments render.""") 
    4956 
    5057    expand_tabs = True 
    5158 
    52     QUALITY_RATIO = 9 
     59    QUALITY_RATIO = 7 
    5360 
     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 
    5476    def __init__(self): 
    5577        self._types = None 
    5678 
     79    # IHTMLPreviewRenderer implementation 
     80 
    5781    def get_quality_ratio(self, mimetype): 
    58         if pygments is None: 
    59             return 0 
    6082        # Extend default MIME type to mode mappings with configured ones 
    6183        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() 
    6885        try: 
    6986            return self._types[mimetype][1] 
    7087        except KeyError: 
    7188            return 0 
    7289 
    7390    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)) 
    7495        try: 
    7596            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) 
    7899        except (KeyError, ValueError): 
    79100            raise Exception("No Pygments lexer found for mime-type '%s'." 
    80101                            % mimetype) 
    81102 
     103    # IPreferencePanelProvider implementation 
    82104 
    83 class PygmentsProcessors(Component): 
    84     implements(IWikiMacroProvider) 
     105    def get_preference_panels(self, req): 
     106        yield ('pygments', 'Pygments Theme') 
    85107 
    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() 
    94110 
    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 
    97115 
    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)) 
    100117 
    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        } 
    103123 
     124    # IRequestHandler implementation 
    104125 
     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 
    105168def _iter_lexerinfo(): 
    106169    for info in LEXERS.itervalues(): 
    107170        yield info 
    108171    for cls in find_plugin_lexers(): 
    109172        yield cls.__module__, cls.name, cls.aliases, cls.filenames, cls.mimetypes 
    110173 
    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 
    119174 
    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 
     175class GenshiHtmlFormatter(HtmlFormatter): 
    126176 
     177    def generate(self, tokensource): 
     178        nocls = self.noclasses 
     179        getcls = self.ttype2class.get 
     180        c2s = self.class2style 
    127181 
    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') 
    153184 
    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

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