Edgewall Software

Ticket #3332: mimeview_refactoring-r3507.diff

File mimeview_refactoring-r3507.diff, 82.1 kB (added by cboos, 2 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` stri