Edgewall Software

Ticket #3332: mimeview_refactoring-r3507.diff

File mimeview_refactoring-r3507.diff, 82.1 KB (added by cboos, 6 years ago)

Initial steps of the refactoring. The new mimeview.api is in place. The Wiki, Attachment and Browser modules are using it. The individual IHTMLPreviewRenderer are still there, but used through a wrapper.

  • trac/attachment.py

     
    2626from trac.config import BoolOption, IntOption 
    2727from trac.core import * 
    2828from trac.env import IEnvironmentSetupParticipant 
    29 from trac.mimeview import * 
     29from trac.mimeview.api import Mimeview, MimeType, FileMimeContent 
    3030from trac.util import get_reporter_id, create_unique_file 
    3131from trac.util.datefmt import format_datetime, pretty_timedelta 
    3232from trac.util.markup import Markup, html 
     
    124124    def parent_href(self, req): 
    125125        return req.href(self.parent_type, self.parent_id) 
    126126 
    127     def _get_title(self): 
     127    def _get_title(self): # TODO: should extend to other `parent_type`s 
    128128        return '%s%s: %s' % (self.parent_type == 'ticket' and '#' or '', 
    129129                             self.parent_id, self.filename) 
    130130    title = property(_get_title) 
     
    227227 
    228228    select = classmethod(select) 
    229229 
    230     def open(self): 
     230    def open(self): # deprecate? 
    231231        self.env.log.debug('Trying to open attachment at %s', self.path) 
    232232        try: 
    233233            fd = open(self.path, 'rb') 
     
    505505                                 'author': get_reporter_id(req)} 
    506506 
    507507    def _render_view(self, req, attachment): 
     508        # FIXME: perm_map should extend to other `parent_type`s 
    508509        perm_map = {'ticket': 'TICKET_VIEW', 'wiki': 'WIKI_VIEW'} 
    509510        req.perm.assert_permission(perm_map[attachment.parent_type]) 
    510511 
     
    522523        if req.perm.has_permission(perm_map[attachment.parent_type]): 
    523524            req.hdf['attachment.can_delete'] = 1 
    524525 
    525         fd = attachment.open() 
    526         try: 
    527             mimeview = Mimeview(self.env) 
     526        format = req.args.get('format') 
     527        raw_href = attachment.href(req, format='raw') 
    528528 
    529             # MIME type detection 
    530             str_data = fd.read(1000) 
    531             fd.seek(0) 
    532              
    533             binary = is_binary(str_data) 
    534             mime_type = mimeview.get_mimetype(attachment.filename, str_data) 
     529        mimecontent = FileMimeContent(self.env, attachment.path, raw_href, 
     530                                      'Attachment') 
     531        can_render_as_txt = self.render_unsafe_content and \ 
     532                             not mimecontent.is_binary 
    535533 
    536             # Eventually send the file directly 
    537             format = req.args.get('format') 
    538             if format in ('raw', 'txt'): 
    539                 if not self.render_unsafe_content and not binary: 
    540                     # Force browser to download HTML/SVG/etc pages that may 
    541                     # contain malicious code enabling XSS attacks 
    542                     req.send_header('Content-Disposition', 'attachment;' + 
    543                                     'filename=' + attachment.filename) 
    544                 if not mime_type or (self.render_unsafe_content and \ 
    545                                      not binary and format == 'txt'): 
    546                     mime_type = 'text/plain' 
    547                 if 'charset=' not in mime_type: 
    548                     charset = mimeview.get_charset(str_data, mime_type) 
    549                     mime_type = mime_type + '; charset=' + charset 
    550                 req.send_file(attachment.path, mime_type) 
     534        # Eventually send the file directly 
     535        if format in ('raw', 'txt'): 
     536            if not self.render_unsafe_content and not mimecontent.is_binary: 
     537                # Force browser to download HTML/SVG/etc pages that may 
     538                # contain malicious code enabling XSS attacks 
     539                req.send_header('Content-Disposition', 
     540                                'attachment;filename=%s' % attachment.filename) 
     541            if not mimecontent.type.is_known or \ 
     542                   (format == 'txt' and can_render_as_txt): 
     543                # Force the content to be displayed as text 
     544                type = MimeType('text/plain', mimecontent.encoding) 
     545            else: 
     546                type = mimecontent.type 
     547            req.send_file(attachment.path, type.mimetype_charset) 
    551548 
    552             # add ''Plain Text'' alternate link if needed 
    553             if self.render_unsafe_content and not binary and \ 
    554                mime_type and not mime_type.startswith('text/plain'): 
    555                 plaintext_href = attachment.href(req, format='txt') 
    556                 add_link(req, 'alternate', plaintext_href, 'Plain Text', 
    557                          mime_type) 
     549        mimetype = mimecontent.type.mimetype 
    558550 
    559             # add ''Original Format'' alternate link (always) 
    560             raw_href = attachment.href(req, format='raw') 
    561             add_link(req, 'alternate', raw_href, 'Original Format', mime_type) 
     551        # add ''Plain Text'' alternate link if needed 
     552        if can_render_as_txt and mimecontent.type.is_known and \ 
     553               mimetype != 'text/plain': 
     554            add_link(req, 'alternate', attachment.href(req, format='txt'), 
     555                     'Plain Text', 'text/plain') 
    562556 
    563             self.log.debug("Rendering preview of file %s with mime-type %s" 
    564                            % (attachment.filename, mime_type)) 
     557        # add ''Original Format'' alternate link (always) 
     558        add_link(req, 'alternate', raw_href, 'Original Format', mimetype) 
    565559 
    566             req.hdf['attachment'] = mimeview.preview_to_hdf( 
    567                 req, fd, os.fstat(fd.fileno()).st_size, mime_type, 
    568                 attachment.filename, raw_href, annotations=['lineno']) 
    569         finally: 
    570             fd.close() 
     560        self.log.debug("Rendering preview of file %s with mime-type %s" % \ 
     561                       (attachment.filename, mimetype)) 
    571562 
     563        req.hdf['attachment'] = Mimeview(self.env).preview_to_hdf( 
     564            req, mimecontent, annotations=['lineno']) 
     565 
    572566    def _render_list(self, req, p_type, p_id): 
    573567        self._parent_to_hdf(req, p_type, p_id) 
    574568        req.hdf['attachment'] = { 
  • trac/mimeview/rst.py

     
    2828import re 
    2929 
    3030from trac.core import * 
    31 from trac.mimeview.api import IHTMLPreviewRenderer, content_to_unicode 
     31from trac.mimeview.api import IHTMLPreviewRenderer 
    3232from trac.web.href import Href 
    3333from trac.wiki.formatter import WikiProcessor 
    3434from trac.wiki import WikiSystem 
     
    223223 
    224224        _inliner = rst.states.Inliner() 
    225225        _parser = rst.Parser(inliner=_inliner) 
    226         content = content_to_unicode(self.env, content, mimetype) 
     226        content = unicode(content) 
    227227        content = content.encode('utf-8') 
    228228        html = publish_string(content, writer_name='html', parser=_parser, 
    229229                              settings_overrides={'halt_level': 6}) 
  • trac/mimeview/api.py

     
    1919#         Christian Boos <cboos@neuf.fr> 
    2020 
    2121""" 
    22 The `trac.mimeview` module centralize the intelligence related to 
    23 file metadata, principally concerning the `type` (MIME type) of the content 
    24 and, if relevant, concerning the text encoding (charset) used by the content. 
     22The `trac.mimeview` module centralizes the intelligence related to file 
     23metadata, principally concerning the `type` (MIME type) and the text 
     24encoding (charset) used by the content, if the latter one is relevant. 
    2525 
    2626There are primarily two approaches for getting the MIME type of a given file: 
    2727 * taking advantage of existing conventions for the file name 
    2828 * examining the file content and applying various heuristics 
    2929 
    30 The module also knows how to convert the file content from one type 
    31 to another type. 
     30The module also knows about conversions from one data type to another type, 
     31like conversions to text/html (this is no more a special case). 
    3232 
    33 In some cases, only the `url` pointing to the file's content is actually 
    34 needed, that's why we avoid to read the file's content when it's not needed. 
    35  
    36 The actual `content` to be converted might be a `unicode` object, 
    37 but it can also be the raw byte string (`str`) object, or simply 
    38 an object that can be `read()`. 
     33In order to keep the API simple, we deal with a few classes, 
     34each encapsulating a part of the knowledge related to content. 
     35 * the `MimeType`, for storing the mime type string, the charset, 
     36   but also eventually the name and the commonly used file extension 
     37   for that type 
     38 * the `MimeContent` and `FileMimeContent`, which provide a flexible 
     39   API for handling string and file content, respectively. 
     40   Those classes inherit from the abstract `MimeContentBase`, which 
     41   can be used to make new wrapper classes for any kind of content. 
     42 * the `Conversion` class, which is use to specify conversion from 
     43   one `MimeType` to another 
    3944""" 
    4045 
     46import os 
    4147import re 
    4248from StringIO import StringIO 
    4349 
    4450from trac.config import IntOption, ListOption, Option 
    4551from trac.core import * 
    4652from trac.util import sorted 
    47 from trac.util.text import to_utf8, to_unicode 
     53from trac.util.text import to_utf8, to_unicode, IDENTITY_CHARSET 
    4854from trac.util.markup import escape, Markup, Fragment, html 
    4955 
    5056 
    51 __all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 'Mimeview', 
    52            'content_to_unicode'] 
     57__all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 
     58           'Mimeview', 'MimeType', 'Conversion', 
     59           'MimeContentBase', 'MimeContent', 'FileMimeContent', 
     60           'TEXT_PLAIN', 'TEXT_HTML', 'APPLICATION_OCTET_STREAM'] 
    5361 
    5462 
    5563# Some common MIME types and their associated keywords and/or file extensions 
    5664 
     65APPLICATION_OCTET_STREAM_STR = 'application/octet-stream' 
     66 
    5767KNOWN_MIME_TYPES = { 
    5868    'application/pdf':        ['pdf'], 
    5969    'application/postscript': ['ps'], 
     
    114124    for e in exts: 
    115125        MIME_MAP[e] = t 
    116126 
    117 # Simple builtin autodetection from the content using a regexp 
    118 MODE_RE = re.compile( 
    119     r"#!(?:[/\w.-_]+/)?(\w+)|"               # look for shebang 
    120     r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*- 
    121     r"vim:.*?syntax=(\w+)"                   # look for VIM's syntax=<n> 
    122     ) 
    123127 
    124 def get_mimetype(filename, content=None, mime_map=MIME_MAP): 
    125     """Guess the most probable MIME type of a file with the given name. 
     128# -- a few functions for dealing with MIME types / binary / text content 
     129#    in a simple way (get_mimetype, is_binary, detect_unicode) 
    126130 
     131def get_mimetype_from_filename(filename, mime_map=MIME_MAP): 
     132    """Guess the most probable MIME type of file with the given `filename`. 
     133 
    127134    `filename` is either a filename (the lookup will then use the suffix) 
    128135    or some arbitrary keyword. 
    129      
    130     `content` is either a `str` or an `unicode` string. 
     136    `mime_map` maps keywords to MIME types. 
     137 
     138    Return the MIME type as a string, or `None` if not detected. 
    131139    """ 
    132140    suffix = filename.split('.')[-1] 
    133141    if suffix in mime_map: 
     
    141149            mimetype = mimetypes.guess_type(filename)[0] 
    142150        except: 
    143151            pass 
    144         if not mimetype and content: 
    145             match = re.search(MODE_RE, content[:1000]) 
    146             if match: 
    147                 mode = match.group(1) or match.group(3) or \ 
    148                     match.group(2).lower() 
    149                 if mode in mime_map: 
    150                     # 3) mimetype from the content, using the `MODE_RE` 
    151                     return mime_map[mode] 
    152             else: 
    153                 if is_binary(content): 
    154                     # 4) mimetype from the content, using`is_binary` 
    155                     return 'application/octet-stream' 
    156152        return mimetype 
    157153 
     154# Simple builtin autodetection from the content using a regexp 
     155MODE_RE = re.compile( 
     156    r"#!(?:[/\w.-_]+/)?(\w+)|"               # look for shebang 
     157    r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*- 
     158    r"vim:.*?syntax=(\w+)"                   # look for VIM's syntax=<n> 
     159    ) 
     160 
     161def get_mimetype_from_content(content): 
     162    """Guess the most probable MIME type of file with the given `filename`. 
     163 
     164    `content` is either a `str` or an `unicode` string  
     165 
     166    Return the MIME type as a string, or `None` if not detected. 
     167    """ 
     168    match = re.search(MODE_RE, content[:1000]) 
     169    if match: 
     170        mode = match.group(1) or match.group(3) or \ 
     171            match.group(2).lower() 
     172        if mode in mime_map: 
     173            # 3) mimetype from the content, using the `MODE_RE` 
     174            return mime_map[mode] 
     175    else: 
     176        if is_binary(content): 
     177            # 4) mimetype from the content, using `is_binary` 
     178            return APPLICATION_OCTET_STREAM_STR 
     179 
     180def get_mimetype(filename, content=None, mime_map=MIME_MAP): 
     181    """Auto-detect MIME type either from the `filename` or from the `content`. 
     182    """ 
     183    mimetype = get_mimetype_from_filename(filename, mime_map) 
     184    if not mimetype and content: 
     185        mimetype = get_mimetype_from_content(content) 
     186    return mimetype 
     187 
    158188def is_binary(data): 
    159189    """Detect binary content by checking the first thousand bytes for zeroes. 
    160190 
     
    165195    return '\0' in data[:1000] 
    166196 
    167197def detect_unicode(data): 
    168     """Detect different unicode charsets by looking for BOMs (Byte Order Marks). 
     198    """Detect different unicode charsets by looking for Byte Order Marks. 
    169199 
    170200    Operate obviously only on `str` objects. 
    171201    """ 
     
    178208    else: 
    179209        return None 
    180210 
    181 def content_to_unicode(env, content, mimetype): 
    182     """Retrieve an `unicode` object from a `content` to be previewed""" 
    183     mimeview = Mimeview(env) 
    184     if hasattr(content, 'read'): 
    185         content = content.read(mimeview.max_preview_size) 
    186     return mimeview.to_unicode(content, mimetype) 
    187211 
     212# -- Classes for mimetype, content and conversion 
    188213 
     214class MimeType(object): 
     215    """Representation of a MIME-Type. 
     216 
     217    If the MIME type correspond to text content, the object can also 
     218    store a `charset` information. 
     219 
     220    A MIME type has a `name` and has an `extension` 
     221    that can be used for storing the converted data in a file. 
     222 
     223    All the properties of this class are read-only. 
     224    """ 
     225     
     226    def __init__(self, mimetype, charset=None, name=None, extension=None): 
     227        """The `mimetype` string can eventually embed the `charset`.""" 
     228        self._mimetype = mimetype 
     229        # determine charset 
     230        self._charset = charset 
     231        if not self._charset and self._mimetype: 
     232            sep_idx = mimetype.find(';') 
     233            if sep_idx >= 0: 
     234                self._mimetype = mimetype[:sep_idx].strip() 
     235                charset_idx = mimetype.find('charset=', sep_idx) 
     236                if charset_idx >= 0: 
     237                    self._charset = mimetype[charset_idx+8:].strip() 
     238        self._extension = extension 
     239        self._name = name 
     240 
     241    def __repr__(self): 
     242        return self.mimetype_charset 
     243 
     244    def _get_extension(self): 
     245        if not self._extension: 
     246            self._extension = KNOWN_MIME_TYPES.get(self.mimetype) 
     247            if not self._extension: 
     248                detail = self.mimetype.split('/', 1)[1] 
     249                if detail.startswith('x-'): 
     250                    self._extension = detail[2:] 
     251        return self._extension 
     252 
     253    def _get_mimetype_charset(self): 
     254        """Combine the MIME type and charset information in a single string. 
     255        """ 
     256        if self._mimetype and self._charset: 
     257            return '%s; charset=%s' % (self.mimetype, self.charset) 
     258        else: 
     259            return self.mimetype 
     260 
     261    is_known = property(lambda x: x._mimetype is not None) 
     262    mimetype = property(lambda x: x._mimetype or APPLICATION_OCTET_STREAM_STR, 
     263                        doc="MIME Type string (without charset information)") 
     264    charset = property(lambda x: x._charset, 
     265                       doc="Eventual charset information") 
     266    name = property(lambda x: x._name or x._extension) 
     267    extension = property(_get_extension) 
     268    mimetype_charset = property(_get_mimetype_charset) 
     269 
     270    def match(self, other, regexp=False): 
     271        """Compare MIME type string only. 
     272 
     273        If `regexp` is set, `self.mimetype` is used as a regexp. 
     274        """ 
     275        if not isinstance(other, MimeType): 
     276            return False 
     277        if regexp: 
     278            return re.match(self.mimetype, other.mimetype) 
     279        else: 
     280            return self.mimetype == other.mimetype 
     281 
     282 
     283TEXT_PLAIN = MimeType('text/plain', 'utf-8', 'Plain Text', 'txt') 
     284TEXT_HTML = MimeType('text/html', 'utf-8', 'HTML', 'html') 
     285APPLICATION_OCTET_STREAM = MimeType(APPLICATION_OCTET_STREAM_STR, 
     286                                    IDENTITY_CHARSET, 
     287                                    'Undefined (binary)', 'bin')  
     288 
     289 
     290class MimeContentBase(object): 
     291    """An abstract MIME content, with an associated MimeType. 
     292 
     293    Such an object has means to auto-detect both the MIME Type and 
     294    the `encoding` of its content. 
     295 
     296    That `encoding` is more reliable than the `type.charset` information. 
     297    There are additional consistency checks that are performed, and it 
     298    can be `None` if the content is an `unicode` object. 
     299 
     300    The content itself can be accessed in various ways: through 
     301    the iterator protocol, the len() and unicode() operators... 
     302    """ 
     303 
     304    def __init__(self, env, mimetype=None, filename=None, url=None): 
     305        """ 
     306        `mimetype` can be specified as a `MimeType` object, 
     307        or as string, which will then be a hint about the content. 
     308 
     309        If the mimetype is not specified or equal to 
     310        "application/octet-stream", then it will be auto-detected when needed. 
     311 
     312        In case auto-detection fails, APPLICATION_OCTET_STREAM will be the 
     313        corresponding MIME type. 
     314 
     315        The `filename` is simply a suggested basename for that content. 
     316 
     317        The `url` is a link for retrieving the raw content directly 
     318        from the server. This can be useful for converters that can 
     319        provide links to objects, instead of having to expand the 
     320        content inline. 
     321        """ 
     322        self.env = env 
     323        if isinstance(mimetype, basestring): 
     324            mimetype = MimeType(mimetype) 
     325        self._type = mimetype 
     326        self._filename = filename 
     327        self._url = url 
     328        self._binary = None 
     329        self._encoding = False 
     330 
     331    def __repr__(self): 
     332        return '<%s %s "%s">' % (self.__class__.__name__, self._type, 
     333                                 self._filename or self._url) 
     334 
     335    def __unicode__(self): 
     336        """Return the `unicode` object corresponding to the content.""" 
     337        return to_unicode(self.content, self.encoding) 
     338        # Note: this does the right thing if the content is already `unicode` 
     339 
     340 
     341    def _is_binary(self): 
     342        """An heuristic to guess whether the content is binary data or not. 
     343 
     344        This will trigger the retrieval of an `excerpt` of the content. 
     345        """ 
     346        if self._binary is None: 
     347            mimetype = self.type.mimetype 
     348            self._binary = is_binary(self.excerpt) or \ 
     349                           (self.type.is_known and  
     350                            mimetype == APPLICATION_OCTET_STREAM_STR) or \ 
     351                            mimetype in Mimeview(self.env).treat_as_binary 
     352        return self._binary 
     353 
     354    def _get_type(self): 
     355        """Get or determine the MimeType corresponding to this content. 
     356 
     357        An `excerpt` of the content will be examined if needed. 
     358        """ 
     359        if self._type is None: # not set 
     360            mimetype = None 
     361            if self.filename: 
     362                mimemap = Mimeview(self.env).mimemap 
     363                mimetype = get_mimetype_from_filename(self.filename, mimemap) 
     364            if not mimetype: 
     365                mimetype = get_mimetype_from_content(self.excerpt) 
     366            if not mimetype: 
     367                pass # TODO 0.11: go through IMimeTypeDetectors 
     368            self._type = MimeType(mimetype) 
     369        return self._type 
     370 
     371    def _set_type(self, type): 
     372        """Simply replace the existing `type` by the given `MimeType` object. 
     373 
     374        If `None` is given, this will force auto-detection the next time 
     375        `type` will be accessed. 
     376 
     377        Can be used for in-place conversion (e.g. ''any'' to text/plain). 
     378        """ 
     379        self._type = type 
     380 
     381    def _get_encoding(self): 
     382        """Get or determine the current encoding of that `content`. 
     383 
     384        The encoding will be determined using this order: 
     385         * from the charset information present in the mimetype information 
     386         * auto-detection of the charset from the `content` 
     387         * if nothing else worked, use the configured `default_charset` 
     388 
     389        If the `content` happens to be a genuine `unicode` object, then 
     390        this returns `None`. 
     391        If the `content` is binary, then the encoding will be the identity 
     392        charset (ISO Latin 1). 
     393        """ 
     394        if self._encoding is False: 
     395            charset = self.type.charset 
     396            if charset: 
     397                self._encoding = charset 
     398            elif isinstance(self.excerpt, str): 
     399                utf_encoding = detect_unicode(self.excerpt) 
     400                if utf_encoding is not None: 
     401                    self._encoding = utf_encoding 
     402                elif self.is_binary: 
     403                    self._encoding = IDENTITY_CHARSET 
     404            elif isinstance(self.excerpt, unicode): 
     405                self._encoding = None 
     406            if self._encoding is False: 
     407                pass # TODO 0.11: go through ICharsetDetectors here 
     408            if self._encoding is False: 
     409                self._encoding = Mimeview(self.env).default_charset 
     410        return self._encoding 
     411     
     412    def _get_content(self): 
     413        """Retrieve all the content. 
     414 
     415        Default implementation based on iterator. If the iterator itself 
     416        is implemented based on the content... reimplement this one! 
     417        """ 
     418        return "".join(self.__iter__()) 
     419 
     420    def read(self): # TODO: remove in 0.11 
     421        return self.content # (compatibility with IHTMLPreviewRenderer) 
     422 
     423    # Methods that need to be reimplemented by subclasses: 
     424 
     425    def __iter__(self): 
     426        """Iterate on chunks of raw content.""" 
     427        raise NotImplementedError 
     428 
     429    def __len__(self): 
     430        """Length of the raw content, in bytes.""" 
     431        raise NotImplementedError 
     432 
     433    def _get_excerpt(self, len=1000): 
     434        """Extracts the first `len` characters from the content.""" 
     435        raise NotImplementedError 
     436             
     437    type = property(fget=lambda x: x._get_type(), 
     438                    fset=lambda x, y: x._set_type(y)) 
     439    is_binary = property(lambda x: x._is_binary()) 
     440    encoding = property(lambda x: x._get_encoding()) 
     441    excerpt = property(lambda x: x._get_excerpt()) 
     442    content = property(lambda x: x._get_content()) 
     443    filename = property(lambda x: x._filename) 
     444    url = property(lambda x: x._url) 
     445 
     446 
     447class MimeContent(MimeContentBase): 
     448    """MIME-typed content wrapper for a basestring.""" 
     449 
     450    def __init__(self, env, content, mimetype, filename='file', url=None): 
     451        MimeContentBase.__init__(self, env, mimetype, filename, url) 
     452        self._content = content 
     453 
     454    # Reimplemented methods 
     455 
     456    def _get_content(self): 
     457        """Retrieve the wrapped content. 
     458 
     459        Note: therefore this *might* be an `unicode` object. 
     460        Remember that in this case, `encoding` will be `None`. 
     461        """ 
     462        return self._content 
     463 
     464    def __iter__(self): 
     465        """Iterate on chunks of content. 
     466 
     467        If the content `is_binary` property is `False`, those chunks will 
     468        be lines, with the line endings kept. 
     469        """ 
     470        if self.is_binary: 
     471            buf = StringIO(self.content) 
     472            chunk = buf.read(1000) 
     473            while chunk: 
     474                yield chunk 
     475                chunk = buf.read(1000) 
     476        else: 
     477            for line in self.content.splitlines(True): 
     478                yield line 
     479 
     480    def __len__(self): 
     481        """Length of the content, in characters.""" 
     482        return len(self.content) 
     483 
     484    def _get_excerpt(self, len=1000): 
     485        """Extracts the first `len` characters from the content.""" 
     486        return self._content[:len] 
     487 
     488 
     489class FileMimeContent(MimeContentBase): 
     490    """MIME-typed content wrapper for a file.""" 
     491 
     492    def __init__(self, env, path, url=None, kind='File', mimetype=None): 
     493        self._fd = None 
     494        self._path = path 
     495        self._kind = kind 
     496        self._excerpt = None 
     497        MimeContentBase.__init__(self, env, mimetype, os.path.basename(path), 
     498                                 url) 
     499    def __del__(self): 
     500        if self._fd: 
     501            self._fd.close() 
     502 
     503    def _ensure_open(self): 
     504        if not self._fd: 
     505            try: 
     506                self._fd = open(self._path) 
     507            except IOError: 
     508                raise TracError('%s "%s" not found' % (self._kind, 
     509                                                       self._filename)) 
     510    # Reimplemented methods 
     511     
     512    def __iter__(self): 
     513        """Iterate on chunks of raw content.""" 
     514        chunk = self.excerpt 
     515        while chunk: 
     516            yield chunk 
     517            chunk = self._fd.read(1000) 
     518 
     519    def __len__(self): 
     520        """Length of the raw content, in bytes.""" 
     521        if self._fd: 
     522            stat = os.fstat(self._fd.fileno()) 
     523        else: 
     524            stat = os.stat(self._path) 
     525        return stat.st_size 
     526 
     527    def _get_excerpt(self, len=1000): 
     528        """Extracts the `len` first bytes from the content.""" 
     529        if self._excerpt is None: 
     530            self._ensure_open() 
     531            self._excerpt = self._fd.read(1000) 
     532        return self._excerpt 
     533 
     534         
     535class NoConversion(TracError): 
     536    def __init__(self, msg, from_, to): 
     537        self.msg = msg 
     538        self.from_ = from_ 
     539        self.to = to 
     540 
     541    def message(self): 
     542        return '%s, from %s to %s' % \ 
     543               (self.msg, self.from_.mimetype, self.to.mimetype) 
     544 
     545class Conversion(object): 
     546    """A specification for performing a data conversion. 
     547 
     548    Each conversion is identified by a `key` and targets an output `mimetype`. 
     549 
     550    A conversion also specifies a `quality` ranking, which is a number 
     551    in the range 0 to 9, where 0 means no support and 9 means "perfect" 
     552    support (try to keep 9 available for user defined conversions, 
     553    though nothing will prevent them from using 10 or 100...) 
     554 
     555    Finally, `expand_tabs` indicates whether a tab expansion should precede 
     556    the conversion attempt. 
     557 
     558    e.g. Conversion(key='latex', quality=8, mimetype=MimeType('text/x-tex')) 
     559    """ 
     560 
     561    def __init__(self, key, quality=1, mimetype=TEXT_HTML, expand_tabs=False): 
     562        self.key = key 
     563        self.quality = quality 
     564        self.mimetype = mimetype 
     565        self.expand_tabs = expand_tabs 
     566 
     567    def __repr__(self): 
     568        return '<Conversion "%s" to %s>' % (self.key, self.mimetype)  
     569 
     570 
     571# -- Deprecated (TODO: remove in 0.11) 
     572 
    189573class IHTMLPreviewRenderer(Interface): 
    190574    """Extension point interface for components that add HTML renderers of 
    191575    specific content types to the `Mimeview` component. 
    192576 
    193     (Deprecated) 
     577    Deprecated in 0.10. Implement `IContentConverter` instead. 
    194578    """ 
    195579 
    196580    # implementing classes should set this property to True if they 
     
    198582    expand_tabs = False 
    199583 
    200584    def get_quality_ratio(mimetype): 
    201         """Return the level of support this renderer provides for the `content` 
    202         of the specified MIME type. The return value must be a number between 
    203         0 and 9, where 0 means no support and 9 means "perfect" support. 
    204         """ 
     585        """Return the level of support this renderer provides""" 
    205586 
    206587    def render(req, mimetype, content, filename=None, url=None): 
    207         """Render an XHTML preview of the raw `content`. 
     588        """Render an XHTML preview of the raw `content`.""" 
    208589 
    209         The `content` might be: 
    210          * a `str` object 
    211          * an `unicode` string 
    212          * any object with a `read` method, returning one of the above 
    213590 
    214         It is assumed that the content will correspond to the given `mimetype`. 
     591# -- Interfaces for the extension points 
    215592 
    216         Besides the `content` value, the same content may eventually 
    217         be available through the `filename` or `url` parameters. 
    218         This is useful for renderers that embed objects, using <object> or 
    219         <img> instead of including the content inline. 
    220          
    221         Can return the generated XHTML text as a single string or as an 
    222         iterable that yields strings. In the latter case, the list will 
    223         be considered to correspond to lines of text in the original content. 
     593class IContentConverter(Interface): 
     594    """An extension point interface for generic content conversion. 
     595    """ 
     596 
     597    def get_supported_conversions(mimetype):  
     598        """Check if conversion of `mimetype` is supported by this converter. 
     599 
     600        Return an iterable of `Conversion` objects for which this is 
     601        the case. 
    224602        """ 
    225603 
     604    def convert_content(context, conversion, content):  
     605        """Convert the given `content` using the specified `conversion`. 
     606 
     607        The conversion takes place in the given formatting `context`. 
     608 
     609        A `context` can provide at least a `req` property. 
     610         
     611        Return the converted content as a new `MimeContent` object. 
     612        """  
     613 
    226614class IHTMLPreviewAnnotator(Interface): 
    227615    """Extension point interface for components that can annotate an XHTML 
    228616    representation of file contents with additional information.""" 
     
    240628        annotation data.""" 
    241629 
    242630 
    243 class IContentConverter(Interface): 
    244     """An extension point interface for generic MIME based content 
    245     conversion.""" 
     631# -- The main Mimeview component 
    246632 
    247     def get_supported_conversions(): 
    248         """Return an iterable of tuples in the form (key, name, extension, 
    249         in_mimetype, out_mimetype, quality) representing the MIME conversions 
    250         supported and 
    251         the quality ratio of the conversion in the range 0 to 9, where 0 means 
    252         no support and 9 means "perfect" support. eg. ('latex', 'LaTeX', 'tex', 
    253         'text/x-trac-wiki', 'text/plain', 8)""" 
    254  
    255     def convert_content(req, mimetype, content, key): 
    256         """Convert the given content from mimetype to the output MIME type 
    257         represented by key. Returns a tuple in the form (content, 
    258         output_mime_type) or None if conversion is not possible.""" 
    259  
    260  
    261633class Mimeview(Component): 
    262634    """A generic class to prettify data, typically source code.""" 
    263635 
    264     renderers = ExtensionPoint(IHTMLPreviewRenderer) 
     636    renderers = ExtensionPoint(IHTMLPreviewRenderer) # TODO: remove in 0.11 
    265637    annotators = ExtensionPoint(IHTMLPreviewAnnotator) 
    266638    converters = ExtensionPoint(IContentConverter) 
    267639 
     
    275647        """Maximum file size for HTML preview. (''since 0.9'').""") 
    276648 
    277649    mime_map = ListOption('mimeviewer', 'mime_map', 
    278         'text/x-dylan:dylan,text/x-idl:ice,text/x-ada:ads:adb', 
     650        'text/x-dylan:dylan,text/x-idl:ice,text/x-ada:ads:adb', doc= 
    279651        """List of additional MIME types and keyword mappings. 
    280         Mappings are comma-separated, and for each MIME type, 
    281         there's a colon (":") separated list of associated keywords 
    282         or file extensions. (''since 0.10'').""") 
    283652 
     653        Mappings are comma-separated. Each mapping starts with the mimetype, 
     654        followed by a colon (":") and the (colon separated) list of associated 
     655        keywords or file extensions. (''since 0.10'').""") 
     656 
     657    treat_as_binary = ListOption('mimeviewer', 'treat_as_binary', 
     658        'application/pdf,application/postscript,application/rtf', doc= 
     659        """List of MIME types that should always be treated as binary content. 
     660 
     661        Accounts for the fact that our binary detection heuristic can't 
     662        always work for some kind of binary data. (''since 0.10'').""") 
     663 
    284664    def __init__(self): 
    285665        self._mime_map = None 
    286          
    287     # Public API 
    288666 
    289     def get_supported_conversions(self, mimetype): 
    290         """Return a list of target MIME types in same form as 
    291         `IContentConverter.get_supported_conversions()`, but with the converter 
    292         component appended. Output is ordered from best to worst quality.""" 
     667    def _get_mimemap(self): 
     668        """Extend default extension to MIME type mappings""" 
     669        if not self._mime_map: 
     670            self._mime_map = {} 
     671            self._mime_map.update(MIME_MAP) 
     672            for mapping in self.config['mimeviewer'].getlist('mime_map'): 
     673                if ':' in mapping: 
     674                    assocations = mapping.split(':') 
     675                    mimetype = assocations[0] 
     676                    for keyword in assocations: # mimetype->mimetype on purpose 
     677                        self._mime_map[keyword] = mimetype 
     678        return self._mime_map 
     679 
     680    mimemap = property(_get_mimemap) 
     681 
     682    def lookup(self, keyword, charset=None): 
     683        mimetype = self.mimemap.get(keyword, APPLICATION_OCTET_STREAM_STR) 
     684        return MimeType(mimetype, charset, extension=keyword) 
     685 
     686    # -- MIME type conversion 
     687     
     688    def get_conversions(self, input): 
     689        """Return a list of possible conversions for the `input` MimeType. 
     690 
     691        The returned list contains pair of (conversion, converter) objects, 
     692        ordered from best to worst quality. 
     693        """ 
     694        # Build list of possible conversions 
    293695        converters = [] 
    294696        for converter in self.converters: 
    295             for k, n, e, im, om, q in converter.get_supported_conversions(): 
    296                 if im == mimetype and q > 0: 
    297                     converters.append((k, n, e, im, om, q, converter)) 
    298         converters = sorted(converters, key=lambda i: i[-1], reverse=True) 
    299         return converters 
     697            for conversion in converter.get_supported_conversions(input): 
     698                if conversion.quality > 0: 
     699                    converters.append((conversion, converter)) 
    300700 
    301     def convert_content(self, req, mimetype, content, key, filename=None, 
    302                         url=None): 
    303         """Convert the given content to the target MIME type represented by 
    304         `key`, which can be either a MIME type or a key. Returns a tuple of 
    305         (content, output_mime_type, extension).""" 
    306         if not content: 
    307             return ('', 'text/plain;charset=utf-8') 
     701        # ---- Backward compatibility support for IHTMLPreviewRenderer 
     702        class IHTMLPreviewRendererWrapper(object): 
     703            def __init__(self, renderer): 
     704                self.renderer = renderer 
     705            def __repr__(self): 
     706                return repr(self.renderer) 
     707            def convert_content(self, context, conversion, mimecontent): 
     708                return self.renderer.render( 
     709                    context.req, mimecontent.type.mimetype, 
     710                    mimecontent, # which is read()able  
     711                    mimecontent.filename, mimecontent.url) 
     712        for renderer in self.renderers: 
     713            qr = renderer.get_quality_ratio(input.mimetype) 
     714            if qr > 0: 
     715                expand_tabs = getattr(renderer, 'expand_tabs', False) 
     716                converters.append( 
     717                    (Conversion(key='', quality=qr, mimetype=TEXT_HTML, 
     718                                expand_tabs=expand_tabs), 
     719                     IHTMLPreviewRendererWrapper(renderer))) 
     720        # ---- (to be removed in 0.11) 
    308721 
    309         # Ensure we have a MIME type for this content 
    310         full_mimetype = mimetype 
    311         if not full_mimetype: 
    312             if hasattr(content, 'read'): 
    313                 content = content.read(self.max_preview_size) 
    314             full_mimetype = self.get_mimetype(filename, content) 
    315         if full_mimetype: 
    316             mimetype = full_mimetype.split(';')[0].strip() # split off charset 
    317         else: 
    318             mimetype = full_mimetype = 'text/plain' # fallback if not binary 
     722        return sorted(converters, key=lambda c: c[0].quality, reverse=True) 
    319723 
    320         # Choose best converter 
    321         candidates = list(self.get_supported_conversions(mimetype)) 
    322         candidates = [c for c in candidates if key in (c[0], c[4])] 
     724    def convert(self, context, selector, mimecontent): 
     725        """Convert the `mimecontent` to another MIME type. 
     726 
     727        The conversion to be done is determined by `selector`, 
     728        which can be either directly the desired output MIME type or 
     729        a key identifying the Conversion object. 
     730 
     731        Returns a new MimeContent. 
     732        """ 
     733        # Ensure we have the mimetype and the charset information 
     734        output = self.lookup(selector) 
     735        if not output: 
     736            self.log.debug('No output MIME type selected by "%s"' % selector) 
     737 
     738        # Get all available conversions for the input 
     739        candidates = self.get_conversions(mimecontent.type) 
     740        # Filter those which are matching `selector` 
     741        candidates = [cn_cr for cn_cr in candidates 
     742                      if selector in (cn_cr[0].key, output.mimetype)] 
    323743        if not candidates: 
    324             raise TracError('No available MIME conversions from %s to %s' % 
    325                             (mimetype, key)) 
     744            raise NoConversion('No available MIME conversions', 
     745                               mimecontent.type, output) 
    326746 
    327         # First successful conversion wins 
    328         for ck, name, ext, input_mimettype, output_mimetype, quality, \ 
    329                 converter in candidates: 
    330             output = converter.convert_content(req, mimetype, content, ck) 
    331             if not output: 
    332                 continue 
    333             return (output[0], output[1], ext) 
    334         raise TracError('No available MIME conversions from %s to %s' % 
    335                         (mimetype, key)) 
     747        tab_expanded = None # we don't want to expand tabs more than once. 
    336748 
     749        # First candidate which converts successfully wins. 
     750        for conversion, converter in candidates: 
     751            if conversion.expand_tabs and not tab_expanded: 
     752                tab_expanded = unicode(mimecontent).expandtabs(self.tab_width) 
     753                mimecontent = MimeContent(self.env, tab_expanded, 
     754                                          mimecontent.type) 
     755                self.log.debug('tab expansion performed.') 
     756            try: 
     757                self.log.debug('Attempt conversion %s using %s' % 
     758                               (conversion, converter)) 
     759                res = converter.convert_content(context, conversion, 
     760                                                mimecontent) 
     761                if not res: 
     762                    continue 
     763                return res 
     764            except Exception, e: 
     765                self.log.warning('MIME conversion using %s failed (%s)' 
     766                                 % (converter, e), exc_info=True) 
     767        raise NoConversion('No MIME conversions succeeded', 
     768                           mimecontent.type, output) 
     769 
     770    # -- XHTML rendering and annotations (based on the conversion API) 
     771     
    337772    def get_annotation_types(self): 
    338773        """Generator that returns all available annotation types.""" 
    339774        for annotator in self.annotators: 
    340775            yield annotator.get_annotation_type() 
    341776 
    342     def render(self, req, mimetype, content, filename=None, url=None, 
    343                annotations=None): 
    344         """Render an XHTML preview of the given `content`. 
     777    def render(self, req, mimecontent, annotations=None): 
     778        """Render an XHTML preview of the given `mimecontent`. 
    345779 
    346         `content` is the same as an `IHTMLPreviewRenderer.render`'s 
    347         `content` argument. 
    348  
    349         The specified `mimetype` will be used to select the most appropriate 
    350         `IHTMLPreviewRenderer` implementation available for this MIME type. 
    351         If not given, the MIME type will be infered from the filename or the 
    352         content. 
    353  
    354         Return a string containing the XHTML text. 
     780        Some `annotations` might be requested as well. 
    355781        """ 
    356         if not content: 
    357             return '' 
     782        class ToplevelContext(object): 
     783            def __init__(self, req): 
     784                self.req = req 
     785        result = self.convert(ToplevelContext(req), 'text/html', mimecontent) 
    358786 
    359         # Ensure we have a MIME type for this content 
    360         full_mimetype = mimetype 
    361         if not full_mimetype: 
    362             if hasattr(content, 'read'): 
    363                 content = content.read(self.max_preview_size) 
    364             full_mimetype = self.get_mimetype(filename, content) 
    365         if full_mimetype: 
    366             mimetype = full_mimetype.split(';')[0].strip() # split off charset 
    367         else: 
    368             mimetype = full_mimetype = 'text/plain' # fallback if not binary 
     787        if isinstance(result, Fragment): 
     788            return result             # might be processed further 
     789        elif isinstance(result, basestring): 
     790            self.log.warning('HTML rendering: got %s' % 
     791                             result.__class__.__name__) 
     792            return Markup(to_unicode(result)) # needed for compatibility  
    369793 
    370         # Determine candidate `IHTMLPreviewRenderer`s 
    371         candidates = [] 
    372         for renderer in self.renderers: 
    373             qr = renderer.get_quality_ratio(mimetype) 
    374             if qr > 0: 
    375                 candidates.append((qr, renderer)) 
    376         candidates.sort(lambda x,y: cmp(y[0], x[0])) 
     794        # otherwise, it's an iterable yielding lines 
     795        if annotations: 
     796            return Markup(self._annotate(result, annotations)) 
    377797 
    378         # First candidate which renders successfully wins. 
    379         # Also, we don't want to expand tabs more than once. 
    380         expanded_content = None 
    381         for qr, renderer in candidates: 
    382             try: 
    383                 self.log.debug('Trying to render HTML preview using %s' 
    384                                % renderer.__class__.__name__) 
    385                 # check if we need to perform a tab expansion 
    386                 rendered_content = content 
    387                 if getattr(renderer, 'expand_tabs', False): 
    388                     if expanded_content is None: 
    389                         content = content_to_unicode(self.env, content, 
    390                                                      full_mimetype) 
    391                         expanded_content = content.expandtabs(self.tab_width) 
    392                     rendered_content = expanded_content 
    393                 result = renderer.render(req, full_mimetype, rendered_content, 
    394                                          filename, url) 
    395                 if not result: 
    396                     continue 
    397                 elif isinstance(result, Fragment): 
    398                     return result 
    399                 elif isinstance(result, basestring): 
    400                     return Markup(to_unicode(result)) 
    401                 elif annotations: 
    402                     return Markup(self._annotate(result, annotations)) 
    403                 else: 
    404                     buf = StringIO() 
    405                     buf.write('<div class="code"><pre>') 
    406                     for line in result: 
    407                         buf.write(line + '\n') 
    408                     buf.write('</pre></div>') 
    409                     return Markup(buf.getvalue()) 
    410             except Exception, e: 
    411                 self.log.warning('HTML preview using %s failed (%s)' 
    412                                  % (renderer, e), exc_info=True) 
     798        return html.DIV(html.PRE(Markup(''.join(result))), class_="code") 
    413799 
    414800    def _annotate(self, lines, annotations): 
     801        """Add requested `annotations` to the lines' content.""" 
    415802        buf = StringIO() 
    416803        buf.write('<table class="code"><thead><tr>') 
    417804        annotators = [] 
     
    445832        buf.write('</tbody></table>') 
    446833        return buf.getvalue() 
    447834 
     835    # -- Deprecated API (TODO: remove in 0.11) 
     836 
    448837    def get_max_preview_size(self): 
    449838        """Deprecated: use `max_preview_size` attribute directly.""" 
    450839        return self.max_preview_size 
    451840 
    452     def get_charset(self, content='', mimetype=None): 
    453         """Infer the character encoding from the `content` or the `mimetype`. 
    454  
    455         `content` is either a `str` or an `unicode` object. 
    456          
    457         The charset will be determined using this order: 
    458          * from the charset information present in the `mimetype` argument 
    459          * auto-detection of the charset from the `content` 
    460          * the configured `default_charset`  
    461         """ 
    462         if mimetype: 
    463             ctpos = mimetype.find('charset=') 
    464             if ctpos >= 0: 
    465                 return mimetype[ctpos + 8:].strip() 
    466         if isinstance(content, str): 
    467             utf = detect_unicode(content) 
    468             if utf is not None: 
    469                 return utf 
    470         return self.default_charset 
    471  
    472     def get_mimetype(self, filename, content=None): 
    473         """Infer the MIME type from the `filename` or the `content`. 
    474  
    475         `content` is either a `str` or an `unicode` object. 
    476  
    477         Return the detected MIME type, augmented by the 
    478         charset information (i.e. "<mimetype>; charset=..."), 
    479         or `None` if detection failed. 
    480         """ 
    481         # Extend default extension to MIME type mappings with configured ones 
    482         if not self._mime_map: 
    483             self._mime_map = MIME_MAP 
    484             for mapping in self.config['mimeviewer'].getlist('mime_map'): 
    485                 if ':' in mapping: 
    486                     assocations = mapping.split(':') 
    487                     for keyword in assocations: # Note: [0] kept on purpose 
    488                         self._mime_map[keyword] = assocations[0] 
    489  
    490         mimetype = get_mimetype(filename, content, self._mime_map) 
    491         charset = None 
    492         if mimetype: 
    493             charset = self.get_charset(content, mimetype) 
    494         if mimetype and charset and not 'charset' in mimetype: 
    495             mimetype += '; charset=' + charset 
    496         return mimetype 
    497  
    498841    def to_utf8(self, content, mimetype=None): 
    499842        """Convert an encoded `content` to utf-8. 
    500843 
    501844        ''Deprecated in 0.10. You should use `unicode` strings only.'' 
    502845        """ 
    503         return to_utf8(content, self.get_charset(content, mimetype)) 
     846        return to_utf8(content) 
    504847 
    505     def to_unicode(self, content, mimetype=None, charset=None): 
    506         """Convert `content` (an encoded `str` object) to an `unicode` object. 
     848    # -- Utilities 
    507849 
    508         This calls `trac.util.to_unicode` with the `charset` provided, 
    509         or the one obtained by `Mimeview.get_charset()`. 
     850    def configured_modes_mapping(self, renderer): 
     851        """Utility for configurable custom converters 
     852 
     853        Return a MIME type to `(mode,quality)` mapping for given `option`, 
     854        assuming a format of comma-separated <mimetype>:<mode>:<quality> 
     855        associations. 
     856 
     857        See EnscriptConverter and SilverCityConverter. 
    510858        """ 
    511         if not charset: 
    512             charset = self.get_charset(content, mimetype) 
    513         return to_unicode(content, charset) 
    514  
    515     def configured_modes_mapping(self, renderer): 
    516         """Return a MIME type to `(mode,quality)` mapping for given `option`""" 
    517859        types, option = {}, '%s_modes' % renderer 
    518860        for mapping in self.config['mimeviewer'].getlist(option): 
    519861            if not mapping: 
     
    526868                                 "option." % (mapping, option)) 
    527869        return types 
    528870     
    529     def preview_to_hdf(self, req, content, length, mimetype, filename, 
    530                        url=None, annotations=None): 
    531         """Prepares a rendered preview of the given `content`. 
    532  
    533         Note: `content` will usually be an object with a `read` method. 
    534         """         
    535         if length >= self.max_preview_size: 
     871    def preview_to_hdf(self, req, mimecontent, annotations=None): 
     872        """Prepares a rendered preview of the given `mimecontent`."""         
     873        if len(mimecontent) >= self.max_preview_size: 
    536874            return {'max_file_size_reached': True, 
    537875                    'max_file_size': self.max_preview_size, 
    538                     'raw_href': url} 
     876                    'raw_href': mimecontent.url} 
    539877        else: 
    540             return {'preview': self.render(req, mimetype, content, filename, 
    541                                            url, annotations), 
    542                     'raw_href': url} 
     878            preview_error = preview = None 
     879            try: 
     880                preview = self.render(req, mimecontent, annotations) 
     881            except NoConversion, e: 
     882                preview_error = e.message() 
     883            return {'preview': preview, 
     884                    'preview_error': preview_error, 
     885                    'raw_href': mimecontent.url} 
    543886 
    544     def send_converted(self, req, in_type, content, selector, filename='file'): 
    545         """Helper method for converting `content` and sending it directly. 
     887    def send_converted(self, req, selector, mimecontent): 
     888        """Helper method for converting `mimecontent` and sending it directly. 
    546889 
    547         `selector` can be either a key or a MIME Type.""" 
     890        `selector` can be either a key or the expected output MIME Type. 
     891        """ 
    548892        from trac.web import RequestDone 
    549         content, output_type, ext = self.convert_content(req, in_type, 
    550                                                          content, selector) 
     893        result = self.convert(req, selector, mimecontent) 
    551894        req.send_response(200) 
    552         req.send_header('Content-Type', output_type) 
    553         req.send_header('Content-Disposition', 'filename=%s.%s' % (filename, 
    554                                                                    ext)) 
     895        req.send_header('Content-Type', self.get_content_type(result)) 
     896        req.send_header('Content-Disposition', 
     897                        self.get_content_disposition(result)) 
    555898        req.end_headers() 
    556         req.write(content) 
    557         raise RequestDone         
     899 
     900        self.send_content(req, result) 
     901        raise RequestDone 
     902 
     903    def get_content_type(self, result): 
     904        if isinstance(result, unicode): 
     905            return 'utf-8' 
     906        elif isinstance(result, str): 
     907            return IDENTITY_CHARSET # FIXME: could probably do better... 
     908        else: # MimeContentBase 
     909            return result.encoding or 'utf-8' 
     910 
     911    def get_content_disposition(self, result): 
     912        if isinstance(result, MimeContentBase): 
     913            return 'filename=%s.%s' % (result.filename, result.type.extension) 
     914        else: 
     915            return 'file.'+(is_binary(result) and 'bin' or 'txt') 
     916 
     917    def send_content(self, req, result): 
     918        if isinstance(result, unicode): 
     919            return result.encode('utf-8') 
     920        elif isinstance(result, str): 
     921            return result 
     922        else: # MimeContentBase 
     923            if result.encoding: 
     924                req.write(result.content) 
     925            else: 
     926                req.write(unicode(result).encode('utf-8')) 
     927            # or: for chunk in result: req.write(chunk) 
     928            # TODO: check what's the fastest solution 
    558929         
    559930 
     931# utility for Mimeview._annotate 
    560932def _html_splitlines(lines): 
    561933    """Tracks open and close tags in lines of HTML text and yields lines that 
    562934    have no tags spanning more than one line.""" 
     
    604976    def annotate_line(self, number, content): 
    605977        return '<th id="L%s"><a href="#L%s">%s</a></th>' % (number, number, 
    606978                                                            number) 
     979#        return html.TH(html.A(number, href="#L%s" % number), id=number) 
    607980 
    608981 
    609 # -- Default renderers 
     982# -- Default TEXT_HTML converters (previously ''IHTMLPreviewRenderer'') 
    610983 
    611984class PlainTextRenderer(Component): 
    612     """HTML preview renderer for plain text, and fallback for any kind of text 
    613     for which no more specific renderer is available. 
     985    """Convert text to HTML-escaped text. 
     986 
     987    Will be used as a fallback for any kind of text 
     988    for which no more specific HTML converter is available. 
    614989    """ 
    615     implements(IHTMLPreviewRenderer) 
     990    implements(IContentConverter) 
    616991 
    617     expand_tabs = True 
    618  
    619     TREAT_AS_BINARY = [ 
    620         'application/pdf', 
    621         'application/postscript', 
    622         'application/rtf' 
    623     ] 
    624  
    625     def get_quality_ratio(self, mimetype): 
    626         if mimetype in self.TREAT_AS_BINARY: 
    627             return 0 
    628         return 1 
    629  
    630     def render(self, req, mimetype, content, filename=None, url=None): 
    631         if is_binary(content): 
     992    def get_supported_conversions(self, input): 
     993        if input.mimetype in Mimeview(self.env).treat_as_binary: 
     994            return 
     995        yield Conversion(key='default', 
     996                         quality=TEXT_PLAIN.match(input) and 8 or 1, 
     997                         mimetype=TEXT_HTML, expand_tabs=True) 
     998         
     999    def convert_content(self, context, conversion, mimecontent): 
     1000        if mimecontent.is_binary: 
    6321001            self.env.log.debug("Binary data; no preview available") 
    633             return 
     1002        else: 
     1003            if not TEXT_PLAIN.match(mimecontent.type): 
     1004                self.env.log.debug("Fallback to plain text renderer.") 
     1005            mimecontent.type = MimeType('text/plain', mimecontent.encoding) 
     1006            return mimecontent 
    6341007 
    635         self.env.log.debug("Using default plain text mimeviewer") 
    636         content = content_to_unicode(self.env, content, mimetype) 
    637         for line in content.splitlines(): 
    638             yield escape(line) 
    6391008 
    640  
    6411009class ImageRenderer(Component): 
    642     """Inline image display. Here we don't need the `content` at all.""" 
    643     implements(IHTMLPreviewRenderer) 
     1010    """Inline image display. 
    6441011 
    645     def get_quality_ratio(self, mimetype): 
    646         if mimetype.startswith('image/'): 
    647             return 8 
    648         return 0 
     1012    This renderer doesn't need the actual data at all, only the url. 
     1013    """ 
     1014    implements(IContentConverter) 
    6491015 
    650     def render(self, req, mimetype, content, filename=None, url=None): 
    651         if url: 
    652             return html.DIV(html.IMG(src=url,alt=filename), 
     1016    def get_supported_conversions(self, input): 
     1017        if MimeType('^image/').match(input, regexp=True): 
     1018            yield Conversion(key='image', quality=8, mimetype=TEXT_HTML) 
     1019 
     1020    def convert_content(self, context, conversion, mimecontent): 
     1021        if mimecontent.url: 
     1022            return html.DIV(html.IMG(src=mimecontent.url, 
     1023                                     alt=mimecontent.filename), 
    6531024                            class_="image-file") 
    6541025 
    6551026 
    6561027class WikiTextRenderer(Component): 
    6571028    """Render files containing Trac's own Wiki formatting markup.""" 
    658     implements(IHTMLPreviewRenderer) 
    6591029 
    660     def get_quality_ratio(self, mimetype): 
    661         if mimetype in ('text/x-trac-wiki', 'application/x-trac-wiki'): 
    662             return 8 
    663         return 0 
     1030    implements(IContentConverter) 
    6641031 
    665     def render(self, req, mimetype, content, filename=None, url=None): 
     1032    def get_supported_conversions(self, input): 
     1033        from trac.wiki import TEXT_X_TRAC_WIKI, APPLICATION_X_TRAC_WIKI 
     1034        if TEXT_X_TRAC_WIKI.match(input) or \ 
     1035               APPLICATION_X_TRAC_WIKI.match(input): 
     1036            yield Conversion(key='wiki', quality=8, mimetype=TEXT_HTML) 
     1037 
     1038    def convert_content(self, context, conversion, mimecontent): 
    6661039        from trac.wiki import wiki_to_html 
    667         return wiki_to_html(content_to_unicode(self.env, content, mimetype), 
    668                             self.env, req) 
     1040        return MimeContent(self.env, wiki_to_html(unicode(mimecontent), 
     1041                                                  self.env, context.req), 
     1042                           TEXT_HTML) 
  • trac/mimeview/silvercity.py

     
    108108            raise Exception, err 
    109109 
    110110        # SilverCity does not like unicode strings 
     111        content = content.read() 
    111112        content = content.encode('utf-8') 
    112113         
    113114        # SilverCity generates extra empty line against some types of 
  • trac/mimeview/patch.py

     
    1616#         Ludvig Strigeus 
    1717 
    1818from trac.core import * 
    19 from trac.mimeview.api import content_to_unicode, IHTMLPreviewRenderer, Mimeview 
     19from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 
    2020from trac.util.markup import escape, Markup 
    2121from trac.web.chrome import add_stylesheet 
    2222 
     
    6565    def render(self, req, mimetype, content, filename=None, rev=None): 
    6666        from trac.web.clearsilver import HDFWrapper 
    6767 
    68         content = content_to_unicode(self.env, content, mimetype) 
     68        content = unicode(content) 
    6969        d = self._diff_to_hdf(content.splitlines(), 
    7070                              Mimeview(self.env).tab_width) 
    7171        if not d: 
  • trac/mimeview/enscript.py

     
    135135        cmdline += ' --color -h -q --language=html -p - -E%s' % mode 
    136136        self.env.log.debug("Enscript command line: %s" % cmdline) 
    137137 
     138        content = unicode(content) 
    138139        np = NaivePopen(cmdline, content.encode('utf-8'), capturestderr=1) 
    139140        if np.errorlevel or np.err: 
    140141            err = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel, 
  • trac/mimeview/php.py

     
    2020 
    2121from trac.core import * 
    2222from trac.config import Option 
    23 from trac.mimeview.api import IHTMLPreviewRenderer, content_to_unicode 
     23from trac.mimeview.api import IHTMLPreviewRenderer 
    2424from trac.util import NaivePopen 
    2525from trac.util.markup import Deuglifier 
    2626 
     
    7979        cmdline += ' -sn' 
    8080        self.env.log.debug("PHP command line: %s" % cmdline) 
    8181         
    82         content = content_to_unicode(self.env, content, mimetype) 
     82        content = unicode(content) 
    8383        content = content.encode('utf-8') 
    8484        np = NaivePopen(cmdline, content, capturestderr=1) 
    8585        if np.errorlevel or np.err: 
  • trac/versioncontrol/web_ui/util.py

     
    1616# Author: Jonas Borgström <jonas@edgewall.com> 
    1717#         Christian Boos <cboos@neuf.fr> 
    1818 
     19import posixpath 
    1920import re 
    2021import urllib 
    2122 
    2223from trac.core import TracError 
     24from trac.mimeview.api import MimeContentBase 
    2325from trac.util.datefmt import format_datetime, pretty_timedelta 
    2426from trac.util.text import shorten_line 
    2527from trac.util.markup import escape, html, Markup 
     
    2729from trac.wiki import wiki_to_html, wiki_to_oneliner 
    2830 
    2931__all__ = ['get_changes', 'get_path_links', 'get_path_rev_line', 
    30            'get_existing_node', 'render_node_property'] 
     32           'get_existing_node', 'render_node_property', 
     33           'NodeMimeContent'] 
    3134 
     35 
     36# MimeContent wrapper for repository files 
     37# 
     38# -- This could be part of trac.versioncontrol.api itself, 
     39#    if introducing a dependency on trac.mimeview.api is 
     40#    considered to be OK. 
     41 
     42CHUNK_SIZE = 4096 
     43 
     44class NodeMimeContent(MimeContentBase): 
     45    """Encapsulation of the content of a file in the repository.""" 
     46             
     47    def __init__(self, env, node, url=None): 
     48        self.node = node 
     49        MimeContentBase.__init__(self, env, node.content_type, 
     50                                 posixpath.basename(node.path), url) 
     51        self._excerpt = None 
     52 
     53    def __iter__(self): 
     54        """Get chunks of the byte content""" 
     55        chunk = self.excerpt 
     56        self._excerpt = None 
     57        while True: 
     58            if not chunk: 
     59                return 
     60            yield chunk 
     61            chunk = self._content.read(CHUNK_SIZE) 
     62 
     63    def __len__(self): 
     64        return self.node.get_content_length() 
     65     
     66    def _get_excerpt(self): 
     67        if not self._excerpt: 
     68            self._content = self.node.get_content() 
     69            self._excerpt = self._content.read(CHUNK_SIZE) 
     70        return self._excerpt 
     71             
     72 
    3273def get_changes(env, repos, revs, full=None, req=None, format=None): 
    3374    db = env.get_db_cnx() 
    3475    changes = {} 
  • trac/versioncontrol/web_ui/changeset.py

     
    2626from trac import util 
    2727from trac.config import BoolOption, IntOption 
    2828from trac.core import * 
    29 from trac.mimeview import Mimeview, is_binary 
    3029from trac.perm import IPermissionRequestor 
    3130from trac.Search import ISearchSource, search_to_sql, shorten_result 
    3231from trac.Timeline import ITimelineEventProvider 
     
    3635from trac.versioncontrol import Changeset, Node 
    3736from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff 
    3837from trac.versioncontrol.svn_authz import SubversionAuthorizer 
    39 from trac.versioncontrol.web_ui.util import render_node_property 
     38from trac.versioncontrol.web_ui.util import * 
    4039from trac.web import IRequestHandler 
    4140from trac.web.chrome import INavigationContributor, add_link, add_stylesheet 
    4241from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider, \ 
     
    439438            The list is empty when no differences between comparable files 
    440439            are detected, but the return value is None for non-comparable files. 
    441440            """ 
    442             old_content = old_node.get_content().read() 
    443             if is_binary(old_content): 
     441            old_content = NodeMimeContent(self.env, old_node) 
     442            if old_content.is_binary: 
    444443                return None 
    445444 
    446             new_content = new_node.get_content().read() 
    447             if is_binary(new_content): 
     445            new_content = NodeMimeContent(self.env, new_content) 
     446            if new_content.is_binary: 
    448447                return None 
    449448 
    450             mview = Mimeview(self.env) 
    451             old_content = mview.to_unicode(old_content, old_node.content_type) 
    452             new_content = mview.to_unicode(new_content, new_node.content_type) 
     449            old_content = unicode(old_content) 
     450            new_content = unicode(new_content) 
    453451 
    454452            if old_content != new_content: 
    455453                context = 3 
     
    526524                        'filename=%s.diff' % filename) 
    527525        req.end_headers() 
    528526 
    529         mimeview = Mimeview(self.env) 
    530527        for old_node, new_node, kind, change in repos.get_changes(**diff): 
    531528            # TODO: Property changes 
    532529 
     
    536533 
    537534            new_content = old_content = '' 
    538535            new_node_info = old_node_info = ('','') 
    539             mimeview = Mimeview(self.env) 
    540536 
    541537            if old_node: 
    542                 old_content = old_node.get_content().read() 
    543                 if is_binary(old_content): 
     538                old_content = NodeMimeContent(self.env, old_node) 
     539                if old_content.is_binary: 
    544540                    continue 
    545541                old_node_info = (old_node.path, old_node.rev) 
    546                 old_content = mimeview.to_unicode(old_content, 
    547                                                   old_node.content_type) 
     542                old_content = unicode(old_content) 
    548543            if new_node: 
    549                 new_content = new_node.get_content().read() 
    550                 if is_binary(new_content): 
     544                new_content = NodeMimeContent(self.env, new_node) 
     545                if new_content.is_binary: 
    551546                    continue 
    552547                new_node_info = (new_node.path, new_node.rev) 
    553548                new_path = new_node.path 
    554                 new_content = mimeview.to_unicode(new_content, 
    555                                                   new_node.content_type) 
     549                new_content = unicode(new_content) 
    556550            else: 
    557551                old_node_path = repos.normalize_path(old_node.path) 
    558552                diff_old_path = repos.normalize_path(diff.old_path) 
  • trac/versioncontrol/web_ui/browser.py

     
    2323from trac import util 
    2424from trac.config import ListOption, Option 
    2525from trac.core import * 
    26 from trac.mimeview import Mimeview, is_binary, get_mimetype 
     26from trac.mimeview import Mimeview, MimeType 
    2727from trac.perm import IPermissionRequestor 
    2828from trac.util import sorted, embedded_numbers 
    2929from trac.util.datefmt import http_date, format_datetime, pretty_timedelta 
     
    3636from trac.versioncontrol.web_ui.util import * 
    3737 
    3838 
    39 CHUNK_SIZE = 4096 
    40  
    41  
    4239class BrowserModule(Component): 
    4340 
    4441    implements(INavigationContributor, IPermissionRequestor, IRequestHandler, 
     
    199196    def _render_file(self, req, repos, node, rev=None): 
    200197        req.perm.assert_permission('FILE_VIEW') 
    201198 
    202         mimeview = Mimeview(self.env) 
     199        format = req.args.get('format') 
    203200 
    204         # MIME type detection 
    205         content = node.get_content() 
    206         chunk = content.read(CHUNK_SIZE) 
    207         mime_type = node.content_type 
    208         if not mime_type or mime_type == 'application/octet-stream': 
    209             mime_type = mimeview.get_mimetype(node.name, chunk) or \ 
    210                         mime_type or 'text/plain' 
     201        raw_href = req.href.browser(node.path, rev=rev, format='raw') 
    211202 
     203        mimecontent = NodeMimeContent(self.env, node, raw_href) 
     204 
    212205        # Eventually send the file directly 
    213         format = req.args.get('format') 
    214         if format in ['raw', 'txt']: 
     206        if format in ('raw', 'txt'): 
     207            if format == 'txt': 
     208                type = MimeType('text/plain', mimecontent.encoding) 
     209            else: 
     210                type = mimecontent.type 
    215211            req.send_response(200) 
    216             req.send_header('Content-Type', 
    217                             format == 'txt' and 'text/plain' or mime_type) 
    218             req.send_header('Content-Length', node.content_length) 
     212            req.send_header('Content-Type', type.mimetype_charset) 
     213            req.send_header('Content-Length', len(mimecontent)) 
    219214            req.send_header('Last-Modified', http_date(node.last_modified)) 
    220215            req.end_headers() 
    221216 
    222             while 1: 
    223                 if not chunk: 
    224                     raise RequestDone 
     217            for chunk in mimecontent: 
    225218                req.write(chunk) 
    226                 chunk = content.read(CHUNK_SIZE) 
     219            raise RequestDone 
    227220        else: 
    228221            # The changeset corresponding to the last change on `node`  
    229222            # is more interesting than the `rev` changeset. 
    230             changeset = repos.get_changeset(node.rev) 
     223            changeset = repos.get_changeset(node.created_rev) 
    231224 
    232225            message = changeset.message or '--' 
    233226            if self.config['changeset'].getbool('wiki_format_messages'): 
     
    238231 
    239232            req.hdf['file'] = { 
    240233                'rev': node.rev, 
    241                 'changeset_href': req.href.changeset(node.rev), 
     234                'changeset_href': req.href.changeset(node.created_rev), 
    242235                'date': format_datetime(changeset.date), 
    243236                'age': pretty_timedelta(changeset.date), 
    244237                'size': pretty_size(node.content_length), 
     
    246239                'message': message 
    247240            }  
    248241 
     242            mimetype = mimecontent.type.mimetype 
     243             
    249244            # add ''Plain Text'' alternate link if needed 
    250             if not is_binary(chunk) and mime_type != 'text/plain': 
     245            if not mimecontent.is_binary and mimecontent.type.is_known and \ 
     246                   mimetype != 'text/plain': 
    251247                plain_href = req.href.browser(node.path, rev=rev, format='txt') 
    252248                add_link(req, 'alternate', plain_href, 'Plain Text', 
    253249                         'text/plain') 
    254250 
    255251            # add ''Original Format'' alternate link (always) 
    256             raw_href = req.href.browser(node.path, rev=rev, format='raw') 
    257             add_link(req, 'alternate', raw_href, 'Original Format', mime_type) 
     252            add_link(req, 'alternate', raw_href, 'Original Format', mimetype) 
    258253 
    259254            self.log.debug("Rendering preview of node %s@%s with mime-type %s" 
    260                            % (node.name, str(rev), mime_type)) 
     255                           % (node.name, str(rev), mimetype)) 
    261256 
    262             del content # the remainder of that content is not needed 
     257            req.hdf['file'] = Mimeview(self.env).preview_to_hdf( 
     258                req, mimecontent, annotations=['lineno']) 
    263259 
    264             req.hdf['file'] = mimeview.preview_to_hdf( 
    265                 req, node.get_content(), node.get_content_length(), mime_type, 
    266                 node.created_path, raw_href, annotations=['lineno']) 
    267  
    268260            add_stylesheet(req, 'common/css/code.css') 
    269261 
    270262    # IWikiSyntaxProvider methods 
  • trac/wiki/web_ui.py

     
    3434from trac.web import HTTPNotFound, IRequestHandler 
    3535from trac.wiki.api import IWikiPageManipulator, WikiSystem 
    3636from trac.wiki.model import WikiPage 
    37 from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner 
    38 from trac.mimeview.api import Mimeview, IContentConverter 
     37from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner, \ 
     38                                TEXT_X_TRAC_WIKI, APPLICATION_X_TRAC_WIKI 
     39from trac.mimeview.api import Mimeview, IContentConverter, Conversion, \ 
     40                              MimeContent, TEXT_PLAIN 
    3941 
    4042 
    4143class InvalidWikiPage(TracError): 
     
    5052    page_manipulators = ExtensionPoint(IWikiPageManipulator) 
    5153 
    5254    # IContentConverter methods 
    53     def get_supported_conversions(self): 
    54         yield ('txt', 'Plain Text', 'txt', 'text/x-trac-wiki', 'text/plain', 9) 
    5555 
    56     def convert_content(self, req, mimetype, content, key): 
    57         return (content, 'text/plain;charset=utf-8') 
     56    def get_supported_conversions(self, input): 
     57        if TEXT_X_TRAC_WIKI.match(input) or \ 
     58               APPLICATION_X_TRAC_WIKI.match(input): 
     59            yield Conversion('txt', quality=8, mimetype=TEXT_PLAIN) 
    5860 
     61    def convert_content(self, context, conversion, content): 
     62        return content # identity transform 
     63 
    5964    # INavigationContributor methods 
    6065 
    6166    def get_active_navigation_item(self, req): 
     
    128133        else: 
    129134            format = req.args.get('format') 
    130135            if format: 
    131                 Mimeview(self.env).send_converted(req, 'text/x-trac-wiki', 
    132                                                   page.text, format, page.name) 
     136                wiki_content = MimeContent(self.env, page.text, 
     137                                           TEXT_X_TRAC_WIKI, 
     138                                           filename=page.name) 
     139                Mimeview(self.env).send_converted(req, format, wiki_content) 
    133140            self._render_view(req, db, page) 
    134141 
    135142        req.hdf['wiki.action'] = action 
     
    429436            req.hdf['html.norobots'] = 1 
    430437 
    431438        # Add registered converters 
    432         for conversion in Mimeview(self.env).get_supported_conversions( 
    433                                              'text/x-trac-wiki'): 
     439        for conv, _ in Mimeview(self.env).get_conversions(TEXT_X_TRAC_WIKI): 
     440            if conv.key in ('wiki', 'default'): 
     441                continue 
    434442            conversion_href = req.href.wiki(page.name, version=version, 
    435                                             format=conversion[0]) 
    436             add_link(req, 'alternate', conversion_href, conversion[1], 
    437                      conversion[3]) 
     443                                            format=conv.key) 
     444            add_link(req, 'alternate', conversion_href, 
     445                     conv.mimetype.name, conv.mimetype.mimetype_charset) 
    438446 
    439447        latest_page = WikiPage(self.env, page.name) 
    440448        req.hdf['wiki'] = {'exists': page.exists, 
  • trac/wiki/formatter.py

     
    3131from trac.util.markup import escape, Markup, Element, html 
    3232 
    3333__all__ = ['wiki_to_html', 'wiki_to_oneliner', 'wiki_to_outline', 
    34            'wiki_to_link', 'Formatter' ] 
     34           'wiki_to_link', 'Formatter', 
     35           'TEXT_X_TRAC_WIKI', 'APPLICATION_X_TRAC_WIKI'] 
    3536 
    3637 
     38TEXT_X_TRAC_WIKI = MimeType('text/x-trac-wiki', 
     39                            name='Trac Wiki Text', extension='txt') 
     40 
     41APPLICATION_X_TRAC_WIKI = MimeType('application/x-trac-wiki', 
     42                                   name='Trac Wiki Text', extension='txt') 
     43 
     44 
    3745def system_message(msg, text=None): 
    3846    return html.DIV(html.STRONG(msg), text and html.PRE(text), 
    3947                    class_="system-message") 
     
    4351 
    4452    _code_block_re = re.compile('^<div(?:\s+class="([^"]+)")?>(.*)</div>$') 
    4553 
    46     def __init__(self, env, name): 
    47         # TODO: transmit `formatter` argument 
    48         self.env = env 
     54    def __init__(self, formatter, name): 
     55        self.formatter = formatter 
     56        self.env = formatter.env 
    4957        self.name = name 
    5058        self.error = None 
    5159        self.macro_provider = None 
     60        self.mimetype = None 
    5261 
    5362        builtin_processors = {'html': self._html_processor, 
    5463                              'default': self._default_processor, 
     
    6574                        break 
    6675        if not self.processor: 
    6776            # Find a matching mimeview renderer 
    68             from trac.mimeview.api import Mimeview 
    69             mimetype = Mimeview(self.env).get_mimetype(self.name) 
    70             if mimetype: 
    71                 self.name = mimetype 
     77            mimetype = Mimeview(self.env).lookup(self.name) 
     78            if not APPLICATION_OCTET_STREAM.match(mimetype): 
     79                self.mimetype = mimetype 
    7280                self.processor = self._mimeview_processor 
    7381            else: 
    7482                self.processor = self._default_processor 
     
    94102    # generic processors 
    95103 
    96104    def _macro_processor(self, req, text): 
    97         # TODO: macro should take a `formatter` argument 
     105        # TODO: macro should take a `formatter` argument (0.11) 
    98106        self.env.log.debug('Executing Wiki macro %s by provider %s' 
    99107                           % (self.name, self.macro_provider)) 
    100108        return self.macro_provider.render_macro(req, self.name, text) 
    101109 
    102110    def _mimeview_processor(self, req, text): 
    103         # TODO: transmit context from `formatter` 
    104         return Mimeview(self.env).render(req, self.name, text) 
     111        class FormatterContext(object): 
     112            def __init__(self, req): 
     113                self.req = req 
     114        blockcontent = MimeContent(self.env, text, self.mimetype) 
     115        return Mimeview(self.env).convert(self.formatter, 'text/html', 
     116                                          blockcontent) 
     117                                          
    105118 
    106119    def process(self, req, text, in_paragraph=False): 
    107120        if self.error: 
     
    429442            return '<br />' 
    430443        args = fullmatch.group('macroargs') 
    431444        try: 
    432             macro = WikiProcessor(self.env, name) 
     445            macro = WikiProcessor(self, name) 
    433446            return macro.process(self.req, args, True) 
    434447        except Exception, e: 
    435448            self.env.log.error('Macro %s(%s) failed' % (name, args), 
     
    714727            else: 
    715728                self.code_text += line + os.linesep 
    716729                if not self.code_processor: 
    717                     self.code_processor = WikiProcessor(self.env, 'default') 
     730                    self.code_processor = WikiProcessor(self, 'default') 
    718731        elif line.strip() == Formatter.ENDBLOCK: 
    719732            self.in_code_block -= 1 
    720733            if self.in_code_block == 0 and self.code_processor: 
     
    728741            match = Formatter._processor_re.search(line) 
    729742            if match: 
    730743                name = match.group(1) 
    731                 self.code_processor = WikiProcessor(self.env, name) 
     744                self.code_processor = WikiProcessor(self, name) 
    732745            else: 
    733746                self.code_text += line + os.linesep  
    734                 self.code_processor = WikiProcessor(self.env, 'default') 
     747                self.code_processor = WikiProcessor(self, 'default') 
    735748        else: 
    736749            self.code_text += line + os.linesep 
    737750 
  • trac/web/chrome.py

     
    1717import os 
    1818import re 
    1919 
    20 from trac import mimeview 
    2120from trac.config import * 
    2221from trac.core import * 
    2322from trac.env import IEnvironmentSetupParticipant 
     23from trac.mimeview import get_mimetype 
    2424from trac.util.markup import html 
    2525from trac.web.api import IRequestHandler, HTTPNotFound 
    2626from trac.web.href import Href 
     
    288288                    icon = href.chrome(icon) 
    289289                else: 
    290290                    icon = href.chrome('common', icon) 
    291             mimetype = mimeview.get_mimetype(icon) 
     291            mimetype = get_mimetype(icon) 
    292292            add_link(req, 'icon', icon, mimetype=mimetype) 
    293293            add_link(req, 'shortcut icon', icon, mimetype=mimetype) 
    294294 
  • trac/util/text.py

     
    2525 
    2626 
    2727CRLF = '\r\n' 
     28IDENTITY_CHARSET = 'iso-8859-1' # not iso-8859-15 
    2829 
    2930# -- Unicode 
    3031 
     
    4041    If the `lossy` argument is `True`, which is the default, then 
    4142    we use the 'replace' mode: 
    4243 
    43     If the `lossy` argument is `False`, we fallback to the 'iso-8859-15' 
    44     charset in case of an error (encoding a `str` using 'iso-8859-15' 
     44    If the `lossy` argument is `False`, we fallback to the 'iso-8859-1' 
     45    charset in case of an error (encoding a `str` using 'iso-8859-1' 
    4546    will always work, as there's one Unicode character for each byte of 
    4647    the input). 
    4748    """ 
     
    6566            except UnicodeError: 
    6667                return unicode(text, locale.getpreferredencoding(), errors) 
    6768    except UnicodeError: 
    68         return unicode(text, 'iso-8859-15') 
     69        return unicode(text, IDENTITY_CHARSET) 
    6970 
    7071def unicode_quote(value): 
    7172    """A unicode aware version of urllib.quote""" 
     
    8586    return urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v) 
    8687                      for k, v in params]) 
    8788 
    88 def to_utf8(text, charset='iso-8859-15'): 
     89def to_utf8(text, charset=IDENTITY_CHARSET): 
    8990    """Convert a string to UTF-8, assuming the encoding is either UTF-8, ISO 
    9091    Latin-1, or as specified by the optional `charset` parameter. 
    9192 
     
    101102            u = unicode(text, charset) 
    102103        except UnicodeError: 
    103104            # This should always work 
    104             u = unicode(text, 'iso-8859-15') 
     105            u = unicode(text, IDENTITY_CHARSET) 
    105106        return u.encode('utf-8') 
    106107 
    107108 
  • templates/browser.cs

     
    111111  </table><?cs 
    112112 /if ?><?cs 
    113113  
    114  if:!browser.is_dir ?> 
    115   <div id="preview"><?cs 
    116    if:file.preview ?><?cs 
    117     var:file.preview ?><?cs 
    118    elif:file.max_file_size_reached ?> 
    119     <strong>HTML preview not available</strong>, since the file size exceeds 
    120     <?cs var:file.max_file_size ?> bytes. Try <a href="<?cs 
    121     var:file.raw_href ?>">downloading</a> the file instead.<?cs 
    122    else ?><strong>HTML preview not available</strong>. To view, <a href="<?cs 
    123     var:file.raw_href ?>">download</a> the file.<?cs 
    124    /if ?> 
    125   </div><?cs 
     114 if:!browser.is_dir ?><?cs 
     115  call:html_preview(file) ?><?cs 
    126116 /if ?> 
    127117 
    128118 <div id="help"> 
  • templates/macros.cs

     
    194194 
    195195def:plural(base, count) ?><?cs 
    196196 var:base ?><?cs if:count != 1 ?>s<?cs /if ?><?cs 
     197/def ?><?cs 
     198 
     199def:html_preview(base) ?> 
     200 <div id="preview"><?cs 
     201  if:base.preview ?> 
     202   <?cs var:base.preview ?><?cs 
     203  elif:base.max_file_size_reached ?> 
     204   <strong>HTML preview not available</strong>, since the file size exceeds 
     205   <?cs var:base.max_file_size  ?> bytes. You may <a href="<?cs 
     206     var:base.raw_href ?>">download the file</a> instead.<?cs 
     207  else ?> 
     208   <strong>HTML preview not available</strong>. To view the file, 
     209   <a href="<?cs var:base.raw_href ?>">download the file</a>.<?cs 
     210  /if ?> 
     211 </div><?cs 
    197212/def ?> 
  • templates/attachment.cs

     
    6666   </th></tr><tr> 
    6767   <td class="message"><?cs var:attachment.description ?></td> 
    6868  </tr> 
    69  </tbody></table> 
    70  <div id="preview"><?cs 
    71   if:attachment.preview ?> 
    72    <?cs var:attachment.preview ?><?cs 
    73   elif:attachment.max_file_size_reached ?> 
    74    <strong>HTML preview not available</strong>, since the file size exceeds 
    75    <?cs var:attachment.max_file_size  ?> bytes. You may <a href="<?cs 
    76      var:attachment.raw_href ?>">download the file</a> instead.<?cs 
    77   else ?> 
    78    <strong>HTML preview not available</strong>. To view the file, 
    79    <a href="<?cs var:attachment.raw_href ?>">download the file</a>.<?cs 
    80   /if ?> 
    81  </div> 
     69 </tbody></table><?cs 
     70 call:html_preview(attachment) ?> 
    8271 <?cs if:attachment.can_delete ?><div class="buttons"> 
    8372  <form method="get" action=""><div id="delete"> 
    8473   <input type="hidden" name="action" value="delete" />