Ticket #4246: trac-pygments.3.diff
| File trac-pygments.3.diff, 29.4 KB (added by cmlenz, 2 years ago) |
|---|
-
htdocs/css/code.css
50 50 table.code tbody th :link:hover, table.code tbody th :visited:hover { 51 51 color: #000; 52 52 } 53 table.code tbody td { 54 background: #fff; 53 table.code td { 55 54 font: normal 11px monospace; 56 55 overflow: hidden; 57 56 padding: 1px 2px; 58 57 vertical-align: top; 59 58 } 60 table.code t body tr.hilite th {59 table.code tr.hilite th { 61 60 background: #ccf; 62 61 } 63 table.code t body tr.hilite td {62 table.code tr.hilite td { 64 63 background: #ddf; 65 64 } 66 65 .image-file { background: #eee; padding: .3em } 67 66 .image-file img { background: url(../imggrid.png) } 68 67 69 68 /* Default */ 70 .code-block span { 71 font-family: monospace; 72 } 69 .code-block span { font-family: monospace; } 73 70 74 71 /* Comments */ 75 72 .code-comment, .css_comment, .c_comment, .c_commentdoc, .c_commentline, -
trac/db_default.py
396 396 'trac.attachment', 'trac.db.mysql_backend', 397 397 'trac.db.postgres_backend', 'trac.db.sqlite_backend', 398 398 'trac.mimeview.enscript', 'trac.mimeview.patch', 399 'trac.mimeview.php', 'trac.mimeview.rst', 400 'trac.mimeview.silvercity', 'trac.mimeview.txtl', 399 'trac.mimeview.php', 'trac.mimeview.pygment', 400 'trac.mimeview.rst', 'trac.mimeview.silvercity', 401 'trac.mimeview.txtl', 401 402 'trac.prefs.web_ui', 'trac.search.web_ui', 402 403 'trac.ticket.admin', 'trac.ticket.query', 403 404 'trac.ticket.report', 'trac.ticket.roadmap', -
trac/mimeview/api.py
59 59 import re 60 60 from StringIO import StringIO 61 61 62 from genshi.core import escape, Markup, Stream 62 from genshi import escape, Markup, Stream 63 from genshi.core import TEXT, START, END, START_NS, END_NS 63 64 from genshi.builder import Fragment, tag 65 from genshi.input import HTMLParser 64 66 65 67 from trac.config import IntOption, ListOption, Option 66 68 from trac.core import * … … 230 232 # support text content where Trac should expand tabs into spaces 231 233 expand_tabs = False 232 234 235 # indicate whether the output of this renderer is source code that can 236 # be decorated with annotations 237 returns_source = False 238 233 239 def get_quality_ratio(mimetype): 234 240 """Return the level of support this renderer provides for the `content` 235 241 of the specified MIME type. The return value must be a number between … … 256 262 be considered to correspond to lines of text in the original content. 257 263 """ 258 264 265 259 266 class IHTMLPreviewAnnotator(Interface): 260 267 """Extension point interface for components that can annotate an XHTML 261 268 representation of file contents with additional information.""" … … 268 275 let the user toggle the appearance of the annotation type. 269 276 """ 270 277 271 def annotate_ line(number, content):278 def annotate_row(number, content): 272 279 """Return the XHTML markup for the table cell that contains the 273 280 annotation data.""" 274 281 … … 438 445 try: 439 446 self.log.debug('Trying to render HTML preview using %s' 440 447 % renderer.__class__.__name__) 448 441 449 # check if we need to perform a tab expansion 442 450 rendered_content = content 443 451 if getattr(renderer, 'expand_tabs', False): … … 446 454 full_mimetype) 447 455 expanded_content = content.expandtabs(self.tab_width) 448 456 rendered_content = expanded_content 457 449 458 result = renderer.render(req, full_mimetype, rendered_content, 450 459 filename, url) 451 460 if not result: 452 461 continue 453 elif isinstance(result, (Fragment, Stream)): 454 return result 455 elif isinstance(result, basestring): 456 return Markup(to_unicode(result)) 457 elif annotations: 462 463 if not getattr(renderer, 'returns_source', False): 464 if isinstance(result, basestring): 465 if not isinstance(result, unicode): 466 result = to_unicode(result) 467 return Markup(to_unicode(result)) 468 elif isinstance(result, Fragment): 469 return result.generate() 470 else: 471 return result 472 473 if annotations: 458 474 m = req.args.get('marks') 459 return Markup(self._annotate(result, annotations, 460 m and Ranges(m))) 475 return self._annotate(result, annotations, m and Ranges(m)) 461 476 else: 462 buf = StringIO() 463 buf.write('<div class="code"><pre>') 464 for line in result: 465 buf.write(line + '\n') 466 buf.write('</pre></div>') 467 return Markup(buf.getvalue()) 477 return tag.div(class_='code')(tag.pre(result)).generate() 478 468 479 except Exception, e: 469 480 self.log.warning('HTML preview using %s failed (%s)' 470 481 % (renderer, e), exc_info=True) 471 482 472 def _annotate(self, lines, annotations, marks=None): 473 buf = StringIO() 474 buf.write('<table class="code"><thead><tr>') 475 annotators = [] 483 def _annotate(self, stream, annotations, marks=None): 484 annotators, annotypes = [], [] 476 485 for annotator in self.annotators: 477 atype, alabel, adesc= annotator.get_annotation_type()486 atype, alabel, _ = annotator.get_annotation_type() 478 487 if atype in annotations: 479 buf.write('<th class="%s">%s</th>' %(atype, alabel))488 annotypes.append((atype, alabel)) 480 489 annotators.append(annotator) 481 buf.write('<th class="content"> </th>')482 buf.write('</tr></thead><tbody>')483 490 484 space_re = re.compile('(?P<spaces> (?: +))|' 485 '^(?P<tag><\w+.*?>)?( )') 486 def htmlify(match): 487 m = match.group('spaces') 488 if m: 489 div, mod = divmod(len(m), 2) 490 return div * ' ' + mod * ' ' 491 return (match.group('tag') or '') + ' ' 491 if isinstance(stream, list): 492 stream = HTMLParser(StringIO('\n'.join(stream))) 492 493 493 num = -1 494 for num, line in enumerate(_html_splitlines(lines)): 495 cells = [] 496 for annotator in annotators: 497 cells.append(annotator.annotate_line(num + 1, line)) 498 cells.append('<td>%s</td>\n' % space_re.sub(htmlify, line)) 499 if marks and num+1 in marks: 500 buf.write('<tr class="%s">' % ('hilite',) + 501 '\n'.join(cells) + '</tr>') 502 else: 503 buf.write('<tr>' + '\n'.join(cells) + '</tr>') 504 else: 505 if num < 0: 506 return '' 507 buf.write('</tbody></table>') 508 return buf.getvalue() 494 def _head_row(): 495 return tag.re( 496 [tag.th(alabel, class_=atype) for atype, alabel in annotypes] + 497 [tag.th(u'\xa0', class_='content')] 498 ) 509 499 500 def _body_rows(): 501 for idx, line in enumerate(_group_lines(stream)): 502 row = tag.tr() 503 if marks and idx + 1 in marks: 504 row(class_='hilite') 505 for annotator in annotators: 506 annotator.annotate_row(row, idx + 1, line) 507 row.append(tag.td(line)) 508 yield row 509 510 return tag.table(class_='code')( 511 tag.thead(_head_row()), 512 tag.tbody(_body_rows()) 513 ) 514 510 515 def get_max_preview_size(self): 511 516 """Deprecated: use `max_preview_size` attribute directly.""" 512 517 return self.max_preview_size … … 616 621 ext)) 617 622 req.end_headers() 618 623 req.write(content) 619 raise RequestDone 620 624 raise RequestDone 621 625 622 def _html_splitlines(lines):623 """Tracks open and close tags in lines of HTML text and yields lines that624 have no tags spanning more than one line."""625 open_tag_re = re.compile(r'<(\w+)(\s.*?)?[^/]?>')626 close_tag_re = re.compile(r'</(\w+)>')627 open_tags = []628 for line in lines:629 # Reopen tags still open from the previous line630 for tag in open_tags:631 line = tag.group(0) + line632 open_tags = []633 626 634 # Find all tags opened on this line 635 for tag in open_tag_re.finditer(line): 636 open_tags.append(tag) 627 def _group_lines(stream): 628 space_re = re.compile('(?P<spaces> (?: +))|^(?P<tag><\w+.*?>)?( )') 629 def pad_spaces(match): 630 m = match.group('spaces') 631 if m: 632 div, mod = divmod(len(m), 2) 633 return div * u'\xa0 ' + mod * u'\xa0' 634 return (match.group('tag') or '') + u'\xa0' 637 635 638 open_tags.reverse() 636 def _generate(): 637 stack = [] 638 def _reverse(): 639 for event in reversed(stack): 640 if event[0] is START: 641 yield END, event[1][0], event[2] 642 else: 643 yield END_NS, event[1][0], event[2] 639 644 640 # Find all tags closed on this line 641 for ctag in close_tag_re.finditer(line): 642 for otag in open_tags: 643 if otag.group(1) == ctag.group(1): 644 open_tags.remove(otag) 645 break 645 for kind, data, pos in stream: 646 if kind is TEXT: 647 lines = data.splitlines(True) 648 for e in stack: 649 yield e 650 yield kind, lines.pop(0).rstrip('\n'), pos 651 for e in _reverse(): 652 yield e 653 if '\n' in data: 654 yield TEXT, '\n', pos 655 for line in lines: 656 for event in stack: 657 yield event 658 yield kind, line.rstrip('\n'), pos 659 if line.endswith('\n'): 660 for e in _reverse(): 661 yield e 662 yield TEXT, '\n', pos 663 else: 664 if kind is START or kind is START_NS: 665 stack.append((kind, data, pos)) 666 elif kind is END or kind is END_NS: 667 stack.pop() 668 else: 669 yield kind, data, pos 646 670 647 # Close all tags still open at the end of line, they'll get reopened at 648 # the beginning of the next line 649 for tag in open_tags: 650 line += '</%s>' % tag.group(1) 671 buf = [] 672 for kind, data, pos in _generate(): 673 if kind is TEXT and data == '\n': 674 yield Stream(buf[:]) 675 del buf[:] 676 else: 677 if kind is TEXT: 678 data = space_re.sub(pad_spaces, data) 679 buf.append((kind, data, pos)) 680 if buf: 681 yield Stream(buf[:]) 651 682 652 yield line653 683 654 655 684 # -- Default annotators 656 685 657 686 class LineNumberAnnotator(Component): … … 663 692 def get_annotation_type(self): 664 693 return 'lineno', 'Line', 'Line numbers' 665 694 666 def annotate_line(self, number, content): 667 return '<th id="L%s"><a href="#L%s">%s</a></th>' % (number, number, 668 number) 695 def annotate_row(self, row, lineno, content): 696 row.append(tag.th(id='L%s' % lineno)( 697 tag.a(lineno, href='#L%s' % lineno) 698 )) 669 699 670 700 671 701 # -- Default renderers … … 677 707 implements(IHTMLPreviewRenderer) 678 708 679 709 expand_tabs = True 710 returns_source = True 680 711 681 712 TREAT_AS_BINARY = [ 682 713 'application/pdf', … … 696 727 697 728 self.env.log.debug("Using default plain text mimeviewer") 698 729 content = content_to_unicode(self.env, content, mimetype) 699 for line in content.splitlines( ):700 yield escape(line)730 for line in content.splitlines(True): 731 yield TEXT, line, (None, -1, -1) 701 732 702 733 703 734 class ImageRenderer(Component): … … 712 743 def render(self, req, mimetype, content, filename=None, url=None): 713 744 if url: 714 745 return tag.div(tag.img(src=url, alt=filename), 715 class_= "image-file")746 class_='image-file') 716 747 717 748 718 749 class WikiTextRenderer(Component): -
trac/mimeview/tests/api.py
15 15 16 16 from trac.core import * 17 17 from trac.test import EnvironmentStub 18 from trac.mimeview.api import get_mimetype, _html_splitlines, \ 19 Mimeview, IContentConverter 18 from trac.mimeview.api import get_mimetype, IContentConverter, Mimeview 20 19 20 21 21 class GetMimeTypeTestCase(unittest.TestCase): 22 22 23 23 def test_from_suffix_using_MIME_MAP(self): … … 53 53 def test_from_content_using_is_binary(self): 54 54 self.assertEqual('application/octet-stream', 55 55 get_mimetype('xxx', "abc\0xyz")) 56 57 56 58 class Converter0(Component):59 implements(IContentConverter)60 def get_supported_conversions(self):61 yield ('key0', 'Format 0', 'c0', 'text/x-sample', 'text/html', 8)62 57 63 class Converter2(Component):64 implements(IContentConverter)65 def get_supported_conversions(self):66 yield ('key2', 'Format 2', 'c2', 'text/x-sample', 'text/html', 2)67 68 class Converter1(Component):69 implements(IContentConverter)70 def get_supported_conversions(self):71 yield ('key1', 'Format 1', 'c1', 'text/x-sample', 'text/html', 4)72 73 58 class MimeviewTestCase(unittest.TestCase): 74 59 75 60 def setUp(self): 76 61 self.env = EnvironmentStub(default_data=True) 77 62 78 def test_html_splitlines_without_markup(self): 79 lines = ['line 1', 'line 2'] 80 self.assertEqual(lines, list(_html_splitlines(lines))) 63 # Make sure we have no external components hanging around in the 64 # component registry 65 from trac.core import ComponentMeta 66 self.old_registry = ComponentMeta._registry 67 ComponentMeta._registry = {} 81 68 82 def test_html_splitlines_with_markup(self): 83 lines = ['<p><b>Hi', 'How are you</b></p>'] 84 result = list(_html_splitlines(lines)) 85 self.assertEqual('<p><b>Hi</b></p>', result[0]) 86 self.assertEqual('<p><b>How are you</b></p>', result[1]) 69 def tearDown(self): 70 # Restore the original component registry 71 from trac.core import ComponentMeta 72 ComponentMeta._registry = self.old_registry 87 73 88 def test_html_splitlines_with_multiline(self):89 """90 Regression test for http://trac.edgewall.org/ticket/265591 """92 lines = ['<span class="p_tripledouble">"""',93 'a <a href="http://google.com">http://google.com</a>/',94 'Test', 'Test', '"""</span>']95 result = list(_html_splitlines(lines))96 self.assertEqual('<span class="p_tripledouble">"""</span>', result[0])97 self.assertEqual('<span class="p_tripledouble">a '98 '<a href="http://google.com">http://google.com</a>/'99 '</span>', result[1])100 self.assertEqual('<span class="p_tripledouble">Test</span>', result[2])101 self.assertEqual('<span class="p_tripledouble">Test</span>', result[3])102 self.assertEqual('<span class="p_tripledouble">"""</span>', result[4])103 104 74 def test_get_supported_conversions(self): 75 class Converter0(Component): 76 implements(IContentConverter) 77 def get_supported_conversions(self): 78 yield 'key0', 'Format 0', 'c0', 'text/x-sample', 'text/html', 8 79 80 class Converter2(Component): 81 implements(IContentConverter) 82 def get_supported_conversions(self): 83 yield 'key2', 'Format 2', 'c2', 'text/x-sample', 'text/html', 2 84 85 class Converter1(Component): 86 implements(IContentConverter) 87 def get_supported_conversions(self): 88 yield 'key1', 'Format 1', 'c1', 'text/x-sample', 'text/html', 4 89 105 90 mimeview = Mimeview(self.env) 106 91 conversions = mimeview.get_supported_conversions('text/x-sample') 107 92 self.assertEqual(Converter0(self.env), conversions[0][-1]) 108 93 self.assertEqual(Converter1(self.env), conversions[1][-1]) 109 94 self.assertEqual(Converter2(self.env), conversions[2][-1]) 110 95 96 111 97 def suite(): 112 98 suite = unittest.TestSuite() 113 99 suite.addTest(unittest.makeSuite(GetMimeTypeTestCase, 'test')) -
trac/mimeview/silvercity.py
41 41 'text/x-chdr': ('CPP', 3), 42 42 'text/x-csrc': ('CPP', 3), 43 43 'text/x-perl': ('Perl', 3), 44 'text/x-php': ('HyperText', 3, {'asp.default.language': 4}),45 'application/x-httpd-php': ('HyperText', 3, {'asp.default.language': 4}),46 'application/x-httpd-php4': ('HyperText', 3, {'asp.default.language': 4}),47 'application/x-httpd-php3': ('HyperText', 3, {'asp.default.language': 4}),44 'text/x-php': ('HyperText', 3, {'asp.default.language': 4}), 45 'application/x-httpd-php': ('HyperText', 3, {'asp.default.language': 4}), 46 'application/x-httpd-php4': ('HyperText', 3, {'asp.default.language': 4}), 47 'application/x-httpd-php3': ('HyperText', 3, {'asp.default.language': 4}), 48 48 'text/x-javascript': ('CPP', 3), # Kludgy. 49 'text/x-psp': ('HyperText', 3, {'asp.default.language': 3}),49 'text/x-psp': ('HyperText', 3, {'asp.default.language': 3}), 50 50 'text/x-python': ('Python', 3), 51 51 'text/x-ruby': ('Ruby', 3), 52 52 'text/x-sql': ('SQL', 3), … … 76 76 (''since 0.10'').""") 77 77 78 78 expand_tabs = True 79 returns_source = True 79 80 80 81 def __init__(self): 81 82 self._types = None -
trac/mimeview/pygment.py
1 # -*- coding: utf-8 -*- 2 # 3 # Copyright (C) 2006 Edgewall Software 4 # Copyright (C) 2006 Matthew Good <matt@matt-good.net> 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 http://trac.edgewall.org/wiki/TracLicense. 10 # 11 # Author: Matthew Good <matt@matt-good.net> 12 13 """Syntax highlighting based on Pygments.""" 14 15 from datetime import datetime 16 import os 17 from pkg_resources import resource_filename 18 import re 19 20 from trac.core import * 21 from trac.config import ListOption, Option 22 from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 23 from trac.prefs import IPreferencePanelProvider 24 from trac.util.datefmt import http_date, localtz 25 from trac.web import IRequestHandler 26 from trac.web.chrome import add_stylesheet 27 28 from genshi import QName, Stream 29 from genshi.core import Attrs, START, END, TEXT 30 31 try: 32 from pygments import get_lexer_by_name 33 from pygments.formatters.html import HtmlFormatter 34 from pygments.styles import get_style_by_name 35 have_pygments = True 36 except ImportError, e: 37 have_pygments = False 38 else: 39 have_pygments = True 40 41 __all__ = ['PygmentsRenderer'] 42 43 44 class PygmentsRenderer(Component): 45 """Syntax highlighting based on Pygments.""" 46 47 implements(IHTMLPreviewRenderer, IPreferencePanelProvider, IRequestHandler) 48 49 default_style = Option('mimeviewer', 'pygments_default_style', 'trac', 50 """The default style to use for Pygments syntax highlighting.""") 51 52 pygments_modes = ListOption('mimeviewer', 'pygments_modes', 53 '', doc= 54 """List of additional MIME types known by Pygments. 55 56 For each, a tuple `mimetype:mode:quality` has to be 57 specified, where `mimetype` is the MIME type, 58 `mode` is the corresponding Pygments mode to be used 59 for the conversion and `quality` is the quality ratio 60 associated to this conversion. That can also be used 61 to override the default quality ratio used by the 62 Pygments render.""") 63 64 expand_tabs = True 65 returns_source = True 66 67 QUALITY_RATIO = 7 68 69 EXAMPLE = """<!DOCTYPE html> 70 <html lang="en"> 71 <head> 72 <title>Hello, world!</title> 73 <script> 74 $(document).ready(function() { 75 $("h1").fadeIn("slow"); 76 }); 77 </script> 78 </head> 79 <body> 80 <h1>Hello, world!</h1> 81 </body> 82 </html>""" 83 84 def __init__(self): 85 self.log.debug("Pygments installed? %r", have_pygments) 86 self._types = None 87 88 # IHTMLPreviewRenderer implementation 89 90 def get_quality_ratio(self, mimetype): 91 # Extend default MIME type to mode mappings with configured ones 92 if self._types is None: 93 self._init_types() 94 try: 95 return self._types[mimetype][1] 96 except KeyError: 97 return 0 98 99 def render(self, req, mimetype, content, filename=None, rev=None): 100 if self._types is None: 101 self._init_types() 102 add_stylesheet(req, '/pygments/%s.css' % 103 req.session.get('pygments_style', self.default_style)) 104 try: 105 mimetype = mimetype.split(';', 1)[0] 106 language = self._types[mimetype][0] 107 return self._generate(language, content) 108 except (KeyError, ValueError): 109 raise Exception("No Pygments lexer found for mime-type '%s'." 110 % mimetype) 111 112 # IPreferencePanelProvider implementation 113 114 def get_preference_panels(self, req): 115 if have_pygments: 116 yield ('pygments', 'Pygments Theme') 117 118 def render_preference_panel(self, req, panel): 119 styles = list(get_all_styles()) 120 121 if req.method == 'POST': 122 style = req.args.get('style') 123 if style and style in styles: 124 req.session['pygments_style'] = style 125 req.redirect(req.href.prefs(panel or None)) 126 127 output = self._generate('html', self.EXAMPLE) 128 return 'prefs_pygments.html', { 129 'output': output, 130 'selection': req.session.get('pygments_style', self.default_style), 131 'styles': styles 132 } 133 134 # IRequestHandler implementation 135 136 def match_request(self, req): 137 if have_pygments: 138 match = re.match(r'/pygments/(\w+)\.css', req.path_info) 139 if match: 140 req.args['style'] = match.group(1) 141 return True 142 143 def process_request(self, req): 144 style = req.args['style'] 145 try: 146 style_cls = get_style_by_name(style) 147 except ValueError, e: 148 raise HTTPNotFound(e) 149 150 parts = style_cls.__module__.split('.') 151 filename = resource_filename('.'.join(parts[:-1]), parts[-1] + '.py') 152 mtime = datetime.fromtimestamp(os.path.getmtime(filename), localtz) 153 last_modified = http_date(mtime) 154 if last_modified == req.get_header('If-Modified-Since'): 155 req.send_response(304) 156 req.end_headers() 157 return 158 159 formatter = HtmlFormatter(style=style_cls) 160 content = u'\n\n'.join([ 161 formatter.get_style_defs('div.code pre'), 162 formatter.get_style_defs('table.code td') 163 ]).encode('utf-8') 164 165 req.send_response(200) 166 req.send_header('Content-Type', 'text/css; charset=utf-8') 167 req.send_header('Last-Modified', last_modified) 168 req.send_header('Content-Length', len(content)) 169 req.write(content) 170 171 # Internal methods 172 173 def _init_types(self): 174 self._types = {} 175 if have_pygments: 176 for _, aliases, _, mimetypes in get_all_lexers(): 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 188 def get_all_lexers(): 189 from pygments.lexers._mapping import LEXERS 190 from pygments.plugin import find_plugin_lexers 191 192 for item in LEXERS.itervalues(): 193 yield item[1:] 194 for cls in find_plugin_lexers(): 195 yield cls.name, cls.aliases, cls.filenames, cls.mimetypes 196 197 def get_all_styles(): 198 from pygments.styles import find_plugin_styles, STYLE_MAP 199 for name in STYLE_MAP: 200 yield name 201 for name, _ in find_plugin_styles(): <202 yield name
