# -*- coding: iso8859-1 -*- # # Copyright (C) 2004-2005 Edgewall Software # Copyright (C) 2004 Daniel Lundin # Copyright (C) 2005 Christopher Lenz # All rights reserved. # # This software is licensed as described in the file COPYING, which # you should have received as part of this distribution. The terms # are also available at http://trac.edgewall.com/license.html. # # This software consists of voluntary contributions made by many # individuals. For the exact contribution history, see the revision # history and logs, available at http://projects.edgewall.com/trac/. # # Author: Daniel Lundin # Christopher Lenz # from __future__ import generators import re try: from cStringIO import StringIO except ImportError: from StringIO import StringIO from trac.core import * from trac.util import enum, escape, to_utf8 __all__ = ['get_charset', 'get_mimetype', 'is_binary', 'detect_unicode', 'Mimeview'] MIME_MAP = { 'css':'text/css', 'html':'text/html', 'txt':'text/plain', 'TXT':'text/plain', 'text':'text/plain', 'README':'text/plain', 'INSTALL':'text/plain', 'AUTHORS':'text/plain', 'COPYING':'text/plain', 'ChangeLog':'text/plain', 'RELEASE':'text/plain', 'ada':'text/x-ada', 'asm':'text/x-asm', 'asp':'text/x-asp', 'awk':'text/x-awk', 'c':'text/x-csrc', 'csh':'application/x-csh', 'diff':'text/x-diff', 'patch':'text/x-diff', 'e':'text/x-eiffel', 'el':'text/x-elisp', 'f':'text/x-fortran', 'h':'text/x-chdr', 'cc':'text/x-c++src', 'CC':'text/x-c++src', 'cpp':'text/x-c++src', 'C':'text/x-c++src', 'hh':'text/x-c++hdr', 'HH':'text/x-c++hdr', 'hpp':'text/x-c++hdr', 'H':'text/x-c++hdr', 'hs':'text/x-haskell', 'ico':'image/x-icon', 'idl':'text/x-idl', 'inf':'text/x-inf', 'java':'text/x-java', 'js':'text/x-javascript', 'ksh':'text/x-ksh', 'lua':'text/x-lua', 'm':'text/x-objc', 'mm':'text/x-objc', 'm4':'text/x-m4', 'make':'text/x-makefile', 'mk':'text/x-makefile', 'Makefile':'text/x-makefile', 'mail':'text/x-mail', 'pas':'text/x-pascal', 'pdf':'application/pdf', 'pl':'text/x-perl', 'pm':'text/x-perl', 'PL':'text/x-perl', 'perl':'text/x-perl', 'php':'text/x-php', 'php4':'text/x-php', 'php3':'text/x-php', 'ps':'application/postscript', 'psp':'text/x-psp', 'py':'text/x-python', 'python':'text/x-python', 'pyx':'text/x-pyrex', 'nroff':'application/x-troff', 'roff':'application/x-troff', 'troff':'application/x-troff', 'rb':'text/x-ruby', 'ruby':'text/x-ruby', 'rfc':'text/x-rfc', 'rst': 'text/x-rst', 'rtf':'application/rtf', 'scm':'text/x-scheme', 'sh':'application/x-sh', 'sql':'text/x-sql', 'svg':'image/svg+xml', 'tcl':'text/x-tcl', 'tex':'text/x-tex', 'txtl': 'text/x-textile', 'textile': 'text/x-textile', 'vb':'text/x-vba', 'vba':'text/x-vba', 'bas':'text/x-vba', 'v':'text/x-verilog', 'verilog':'text/x-verilog', 'vhd':'text/x-vhdl', 'vrml':'model/vrml', 'wrl':'model/vrml', 'xml':'text/xml', 'xs':'text/x-csrc', 'xsl':'text/xsl', 'zsh':'text/x-zsh' } def get_charset(mimetype): """Return the character encoding included in the given content type string, or `None` if `mimetype` is `None` or empty or if no charset information is available. """ if mimetype: ctpos = mimetype.find('charset=') if ctpos >= 0: return mimetype[ctpos + 8:] def get_mimetype(filename): """Guess the most probable MIME type of a file with the given name.""" try: suffix = filename.split('.')[-1] return MIME_MAP[suffix] except KeyError: import mimetypes return mimetypes.guess_type(filename)[0] except: return None def is_binary(str): """Detect binary content by checking the first thousand bytes for zeroes.""" if detect_unicode(str): return False return '\0' in str[:1000] def detect_unicode(data): """Detect different unicode charsets by looking for BOMs (Byte Order Marks).""" if data.startswith('\xff\xfe'): return 'utf-16-le' elif data.startswith('\xfe\xff'): return 'utf-16-be' elif data.startswith('\xef\xbb\xbf'): return 'utf-8' else: return None class IHTMLPreviewRenderer(Interface): """Extension point interface for components that add HTML renderers of specific content types to the `Mimeview` component. """ # implementing classes should set this property to True if they # support text content where Trac should expand tabs into spaces expand_tabs = False def get_quality_ratio(mimetype): """Return the level of support this renderer provides for the content of the specified MIME type. The return value must be a number between 0 and 9, where 0 means no support and 9 means "perfect" support. """ def render(req, mimetype, content, filename=None, rev=None): """Render an XHTML preview of the given content of the specified MIME type. Can return the generated XHTML text as a single string or as an iterable that yields strings. In the latter case, the list will be considered to correspond to lines of text in the original content. The `filename` and `rev` parameters are provided for renderers that embed objects (using or ) instead of included the content inline. """ class IHTMLPreviewAnnotator(Interface): """Extension point interface for components that can annotate an XHTML representation of file contents with additional information.""" def get_annotation_type(): """Return a (type, label, description) tuple that defines the type of annotation and provides human readable names. The `type` element should be unique to the annotator. The `label` element is used as column heading for the table, while `description` is used as a display name to let the user toggle the appearance of the annotation type. """ def annotate_line(number, content): """Return the XHTML markup for the table cell that contains the annotation data.""" class Mimeview(Component): """A generic class to prettify data, typically source code.""" renderers = ExtensionPoint(IHTMLPreviewRenderer) annotators = ExtensionPoint(IHTMLPreviewAnnotator) # Public API def get_annotation_types(self): """Generator that returns all available annotation types.""" for annotator in self.annotators: yield annotator.get_annotation_type() def render(self, req, mimetype, content, filename=None, rev=None, annotations=None): """Render an XHTML preview of the given content of the specified MIME type, selecting the most appropriate `IHTMLPreviewRenderer` implementation available for the given MIME type. Return a string containing the XHTML text. """ if not content: return '' if filename and not mimetype: mimetype = get_mimetype(filename) mimetype = mimetype.split(';')[0].strip() # split off charset expanded_content = None candidates = [] for renderer in self.renderers: qr = renderer.get_quality_ratio(mimetype) if qr > 0: expand_tabs = getattr(renderer, 'expand_tabs', False) if expand_tabs and expanded_content is None: tab_width = int(self.config.get('mimeviewer', 'tab_width')) expanded_content = content.expandtabs(tab_width) if expand_tabs: candidates.append((qr, renderer, expanded_content)) else: candidates.append((qr, renderer, content)) candidates.sort(lambda x,y: cmp(y[0], x[0])) for qr, renderer, content in candidates: try: self.log.debug('Trying to render HTML preview using %s' % renderer.__class__.__name__) result = renderer.render(req, mimetype, content, filename, rev) if not result: continue elif isinstance(result, (str, unicode)): return result elif annotations: return self._annotate(result, annotations) else: buf = StringIO() buf.write('
')
                    for line in result:
                        buf.write(line + '\n')
                    buf.write('
') return buf.getvalue() except Exception, e: self.log.warning('HTML preview using %s failed (%s)' % (renderer, e), exc_info=True) def _annotate(self, lines, annotations): buf = StringIO() buf.write('') annotators = [] for annotator in self.annotators: atype, alabel, adesc = annotator.get_annotation_type() if atype in annotations: buf.write('' % (atype, alabel)) annotators.append(annotator) buf.write('') buf.write('') space_re = re.compile('(?P (?: +))|' '^(?P<\w+.*?>)?( )') def htmlify(match): m = match.group('spaces') if m: div, mod = divmod(len(m), 2) return div * '  ' + mod * ' ' return (match.group('tag') or '') + ' ' for num, line in enum(_html_splitlines(lines)): cells = [] for annotator in annotators: cells.append(annotator.annotate_line(num + 1, line)) cells.append('\n' % space_re.sub(htmlify, line)) buf.write('' + '\n'.join(cells) + '') else: if num == 0: return '' buf.write('
%s 
%s
') return buf.getvalue() def max_preview_size(self): return int(self.config.get('mimeviewer', 'max_preview_size', '262144')) def preview_charset(self, content): return detect_unicode(content) or self.config.get('trac', 'default_charset') def preview_to_hdf(self, req, mimetype, charset, content, filename, detail=None, annotations=None): max_preview_size = self.max_preview_size() if len(content) >= max_preview_size: return {'max_file_size_reached': True, 'max_file_size': max_preview_size} if not is_binary(content): content = to_utf8(content, charset or self.preview_charset(content)) return {'preview': self.render(req, mimetype, content, filename, detail, annotations)} def _html_splitlines(lines): """Tracks open and close tags in lines of HTML text and yields lines that have no tags spanning more than one line.""" open_tag_re = re.compile(r'<(\w+)\s.*?[^/]?>') close_tag_re = re.compile(r'') open_tags = [] for line in lines: # Reopen tags still open from the previous line for tag in open_tags: line = tag.group(0) + line open_tags = [] # Find all tags opened on this line for tag in open_tag_re.finditer(line): open_tags.append(tag) # Find all tags closed on this line for ctag in close_tag_re.finditer(line): for otag in open_tags: if otag.group(1) == ctag.group(1): open_tags.remove(otag) break # Close all tags still open at the end of line, they'll get reopened at # the beginning of the next line for tag in open_tags: line += '' % tag.group(1) yield line class LineNumberAnnotator(Component): """Text annotator that adds a column with line numbers.""" implements(IHTMLPreviewAnnotator) # ITextAnnotator methods def get_annotation_type(self): return 'lineno', 'Line', 'Line numbers' def annotate_line(self, number, content): return '%s' % (number, number, number) class PlainTextRenderer(Component): """HTML preview renderer for plain text, and fallback for any kind of text for which no more specific renderer is available. """ implements(IHTMLPreviewRenderer) expand_tabs = True TREAT_AS_BINARY = [ 'application/pdf', 'application/postscript', 'application/rtf' ] def get_quality_ratio(self, mimetype): if mimetype in self.TREAT_AS_BINARY: return 0 return 1 def render(self, req, mimetype, content, filename=None, rev=None): if is_binary(content): self.env.log.debug("Binary data; no preview available") return self.env.log.debug("Using default plain text mimeviewer") for line in content.splitlines(): yield escape(line) class ImageRenderer(Component): """Inline image display.""" implements(IHTMLPreviewRenderer) def get_quality_ratio(self, mimetype): if mimetype.startswith('image/'): return 8 return 0 def render(self, req, mimetype, content, filename=None, rev=None): src = '?' if rev: src += 'rev=%d&' % rev src += 'format=raw' return '
' % src class WikiTextRenderer(Component): """Render files containing Trac's own Wiki formatting markup.""" implements(IHTMLPreviewRenderer) def get_quality_ratio(self, mimetype): if mimetype in ('text/x-trac-wiki', 'application/x-trac-wiki'): return 8 return 0 def render(self, req, mimetype, content, filename=None, rev=None): from trac.wiki import wiki_to_html return wiki_to_html(content, self.env, req)