Edgewall Software

Ticket #3332: mimeview_api_refactoring-r3612.2.diff

File mimeview_api_refactoring-r3612.2.diff, 119.1 KB (added by cboos, 5 years ago)

Slightly improved version

  • 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, TEXT_PLAIN 
    3030from trac.util import get_reporter_id, create_unique_file 
    3131from trac.util.datefmt import format_datetime, pretty_timedelta 
    3232from trac.util.html 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) 
     
    245245    select = classmethod(select) 
    246246    delete_all = classmethod(delete_all) 
    247247 
    248     def open(self): 
     248    def open(self): # deprecate, use FileMimeContent if necessary 
    249249        self.env.log.debug('Trying to open attachment at %s', self.path) 
    250250        try: 
    251251            fd = open(self.path, 'rb') 
     
    523523                                 'author': get_reporter_id(req)} 
    524524 
    525525    def _render_view(self, req, attachment): 
     526        # FIXME: perm_map should extend to other `parent_type`s 
    526527        perm_map = {'ticket': 'TICKET_VIEW', 'wiki': 'WIKI_VIEW'} 
    527528        req.perm.assert_permission(perm_map[attachment.parent_type]) 
    528529 
     
    533534        req.hdf['attachment'] = attachment_to_hdf(self.env, req, None, 
    534535                                                  attachment) 
    535536        # Override the 'oneliner' 
    536         req.hdf['attachment.description'] = wiki_to_html(attachment.description, 
    537                                                         self.env, req) 
     537        req.hdf['attachment.description'] = wiki_to_html( 
     538            attachment.description, self.env, req) 
    538539 
    539540        perm_map = {'ticket': 'TICKET_ADMIN', 'wiki': 'WIKI_DELETE'} 
    540541        if req.perm.has_permission(perm_map[attachment.parent_type]): 
    541542            req.hdf['attachment.can_delete'] = 1 
    542543 
    543         fd = attachment.open() 
    544         try: 
    545             mimeview = Mimeview(self.env) 
     544        content = FileMimeContent(self.env, attachment.path, 
     545                                  url=attachment.href(req, format='raw'), 
     546                                  kind='Attachment') 
    546547 
    547             # MIME type detection 
    548             str_data = fd.read(1000) 
    549             fd.seek(0) 
    550              
    551             binary = is_binary(str_data) 
    552             mime_type = mimeview.get_mimetype(attachment.filename, str_data) 
     548        # Eventually send the file directly 
     549        format = req.args.get('format') 
     550        if format in ('raw', 'txt'): 
     551            disposition = 'inline' 
     552            if self.render_unsafe_content: 
     553                if content.type.is_unknown or \ 
     554                       (format == 'txt' and not content.is_binary): 
     555                    # Force the content to be displayed as text 
     556                    content.type = TEXT_PLAIN ### SHOULD BE ENOUGH 
     557                    content.type = MimeType('text/plain', content.encoding) 
     558            elif not content.is_binary: 
     559                # Force browser to download HTML/SVG/etc pages that may 
     560                # contain malicious code enabling XSS aattacks 
     561                disposition = 'attachment' 
     562            content.send(req, disposition) 
    553563 
    554             # Eventually send the file directly 
    555             format = req.args.get('format') 
    556             if format in ('raw', 'txt'): 
    557                 if not self.render_unsafe_content and not binary: 
    558                     # Force browser to download HTML/SVG/etc pages that may 
    559                     # contain malicious code enabling XSS attacks 
    560                     req.send_header('Content-Disposition', 'attachment;' + 
    561                                     'filename=' + attachment.filename) 
    562                 if not mime_type or (self.render_unsafe_content and \ 
    563                                      not binary and format == 'txt'): 
    564                     mime_type = 'text/plain' 
    565                 if 'charset=' not in mime_type: 
    566                     charset = mimeview.get_charset(str_data, mime_type) 
    567                     mime_type = mime_type + '; charset=' + charset 
    568                 req.send_file(attachment.path, mime_type) 
     564        # add ''Plain Text'' alternate link if needed 
     565        if self.render_unsafe_content and not \ 
     566               (content.is_binary or 
     567                content.type.is_unknown or  
     568                TEXT_PLAIN.match(content.type)): 
     569            add_link(req, 'alternate', attachment.href(req, format='txt'), 
     570                     TEXT_PLAIN.name, TEXT_PLAIN.mimetype) 
    569571 
    570             # add ''Plain Text'' alternate link if needed 
    571             if self.render_unsafe_content and not binary and \ 
    572                mime_type and not mime_type.startswith('text/plain'): 
    573                 plaintext_href = attachment.href(req, format='txt') 
    574                 add_link(req, 'alternate', plaintext_href, 'Plain Text', 
    575                          mime_type) 
     572        # add ''Original Format'' alternate link (always) 
     573        add_link(req, 'alternate', content.url, 
     574                 'Original Format', content.type.mimetype) 
    576575 
    577             # add ''Original Format'' alternate link (always) 
    578             raw_href = attachment.href(req, format='raw') 
    579             add_link(req, 'alternate', raw_href, 'Original Format', mime_type) 
     576        req.hdf['attachment'] = Mimeview(self.env).preview_to_hdf( 
     577            req, content, annotations=['lineno']) 
    580578 
    581             self.log.debug("Rendering preview of file %s with mime-type %s" 
    582                            % (attachment.filename, mime_type)) 
    583  
    584             req.hdf['attachment'] = mimeview.preview_to_hdf( 
    585                 req, fd, os.fstat(fd.fileno()).st_size, mime_type, 
    586                 attachment.filename, raw_href, annotations=['lineno']) 
    587         finally: 
    588             fd.close() 
    589  
    590579    def _render_list(self, req, p_type, p_id): 
    591580        self._parent_to_hdf(req, p_type, p_id) 
    592581        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 IContentConverter, MimeType, TEXT_HTML, \ 
     32                              Conversion, StringMimeContent 
    3233from trac.util.html import Element 
    3334from trac.web.href import Href 
    3435from trac.wiki.formatter import WikiProcessor 
     
    3839    """ 
    3940    Renders plain text in reStructuredText format as HTML. 
    4041    """ 
    41     implements(IHTMLPreviewRenderer) 
     42    implements(IContentConverter) 
    4243 
    43     def get_quality_ratio(self, mimetype): 
    44         if mimetype == 'text/x-rst': 
    45             return 8 
    46         return 0 
     44    def get_supported_conversions(self, input): 
     45        if input.match(MimeType('text/x-rst')): 
     46            yield Conversion('html', 8, TEXT_HTML) 
    4747 
    48     def render(self, req, mimetype, content, filename=None, rev=None): 
     48    def convert_content(self, context, conversion, content): 
     49        req = context.req 
    4950        try: 
    5051            from docutils import nodes 
    5152            from docutils.core import publish_parts 
     
    180181 
    181182        _inliner = rst.states.Inliner() 
    182183        _parser = rst.Parser(inliner=_inliner) 
    183         content = content_to_unicode(self.env, content, mimetype) 
    184         parts = publish_parts(content, writer_name='html', parser=_parser, 
     184        parts = publish_parts(unicode(content), 
     185                              writer_name='html', parser=_parser, 
    185186                              settings_overrides={'halt_level': 6,  
    186187                                                  'file_insertion_enabled': 0,  
    187188                                                  'raw_enabled': 0}) 
    188         return parts['html_body'] 
     189        return StringMimeContent(self.env, parts['html_body'], 
     190                                 content.type, filename=content.filename, 
     191                                 annotable=False) 
     192 
  • 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 about typed content. 
    2523 
    26 There are primarily two approaches for getting the MIME type of a given file: 
    27  * taking advantage of existing conventions for the file name 
    28  * examining the file content and applying various heuristics 
     24Originally, this was about file metadata, principally  its MIME type and 
     25eventually the text encoding (charset) used by that content. 
    2926 
    30 The module also knows how to convert the file content from one type 
    31 to another type. 
     27Now, this has evolved into managing any kind of typed content, 
     28and deals also with converting a content from one type to another. 
     29A common situation which is now handled part of the general case 
     30is the conversion of any kind of content to a text/html representation. 
    3231 
    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. 
     32In order to keep the API of conversion interface IContentConverter simple, 
     33we introduced a few classes, each encapsulating a part of the knowledge 
     34related to the content and the conversion: 
    3535 
    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()`. 
     36 * the `ContentType` is used to describe the type of a content. 
     37   This is an abstract superclass for: 
     38 
     39   - `MimeType`, used for storing the mime type string, the charset, 
     40     and eventually the name and the commonly used file extension 
     41     for that type 
     42 
     43   - `ObjectType`, used when wrapping an arbitrary Python object 
     44 
     45 * the `AbstractContent`, which wraps access to the actual content, 
     46   and provides a few generic methods, among which `convert()` is 
     47   the most important. 
     48 
     49   - the `ObjectContent`, when the content is an arbitrary Python object 
     50 
     51   - the `MimeContent` abstract class, which offers an uniform API to 
     52     access the data over a wide range of containers: 
     53      * the `StringMimeContent`, for handling string content 
     54        (like `str`, `unicode` or `Markup` objects) 
     55      * the `StructuredMimeContent`, for handling structured content 
     56        (like `Element` objects) 
     57      * the `LineIteratorMimeContent`, for handling line-oriented string 
     58        content (like string iterators or string arrays) 
     59      * the `FileMimeContent`, for handling file content 
     60      * etc. (as an example, the repository layer defines a `NodeMimeContent`) 
     61 
     62     That API provides different ways to get access to the data: 
     63      * chunks(), an iterator for reading chunks of raw content 
     64      * lines(), an iterator for reading lines from text content 
     65      * markup(), when it makes sense to interpret the content as markup 
     66      * `__unicode()__`, for converting the content to an `unicode` object 
     67      * encode(), for converting the content to a `str` object. Note that 
     68        we don't use `__str__` for that on purpose, as we need to be able 
     69        to specify the charset. 
     70      * size(), for getting a hint about the content size without actually 
     71        reading it. 
     72 
     73 * the `Conversion` class, which is used to specify how a given 
     74   `IContentConverter` component will perform a content conversion. 
    3975""" 
    4076 
     77import os 
    4178import re 
    4279from StringIO import StringIO 
    4380 
    4481from trac.config import IntOption, ListOption, Option 
    4582from trac.core import * 
    4683from trac.util import sorted 
    47 from trac.util.text import to_utf8, to_unicode 
     84from trac.util.datefmt import http_date 
     85from trac.util.text import to_utf8, to_unicode, IDENTITY_CHARSET 
    4886from trac.util.html import escape, Markup, Fragment, html 
    4987 
    5088 
    51 __all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 'Mimeview', 
    52            'content_to_unicode'] 
     89__all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 
     90           'IContentConverter', 'IHTMLPreviewAnnotator', 
     91           'IHTMLPreviewRenderer', # deprecated 
     92           'ObjectType', 'MimeType', 
     93           'ObjectContent', 
     94           'MimeContent', 'FileMimeContent', 'StringMimeContent', 
     95           'StructuredMimeContent', 'LineIteratorMimeContent', 
     96           'Mimeview', 'Conversion', 
     97           'TEXT_PLAIN', 'TEXT_HTML', 'TEXT_CSV', 'TEXT_TSV', 
     98           'APPLICATION_RSS_XML', 'APPLICATION_OCTET_STREAM'] 
    5399 
    54100 
    55101# Some common MIME types and their associated keywords and/or file extensions 
    56102 
     103APPLICATION_OCTET_STREAM_STR = 'application/octet-stream' 
     104 
    57105KNOWN_MIME_TYPES = { 
    58106    'application/pdf':        ['pdf'], 
    59107    'application/postscript': ['ps'], 
     
    114162    for e in exts: 
    115163        MIME_MAP[e] = t 
    116164 
    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     ) 
    123165 
    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. 
     166# -- get_mimetype, is_binary, detect_unicode: a few functions for dealing 
     167#    with MIME types, binary and text content in a simple way 
    126168 
     169def get_mimetype_from_filename(filename, mime_map=MIME_MAP): 
     170    """Guess the most probable MIME type of file with the given `filename`. 
     171 
    127172    `filename` is either a filename (the lookup will then use the suffix) 
    128173    or some arbitrary keyword. 
    129      
    130     `content` is either a `str` or an `unicode` string. 
     174    `mime_map` maps keywords to MIME types. 
     175 
     176    Return the MIME type as a string, or `None` if nothing was detected. 
    131177    """ 
    132178    suffix = filename.split('.')[-1] 
    133179    if suffix in mime_map: 
     
    141187            mimetype = mimetypes.guess_type(filename)[0] 
    142188        except: 
    143189            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' 
    156190        return mimetype 
    157191 
     192# Simple builtin autodetection from the content using a regexp 
     193MODE_RE = re.compile( 
     194    # look for /usr/bin/prog1 or /usr/bin/env prog2 
     195    r"#!(?:[/\w.-_]+/)?(?P<prog1>\w+)(?:\s+(?P<prog2>\w+))?|" 
     196    # look for Emacs' -*- mode -*- 
     197    r"-\*-\s*(?:mode:\s*)?(?P<emacsmode>[\w+-]+)\s*-\*-|" 
     198    # look for VIM's syntax=<n> 
     199    r"vim:.*?syntax=(?P<vimsyntax>\w+)" 
     200    ) 
     201 
     202def get_mimetype_from_content(content, mime_map=MIME_MAP): 
     203    """Guess the most probable MIME type of file with the given `filename`. 
     204 
     205    `content` is either a `str` or an `unicode` string  
     206 
     207    Return the MIME type as a string, or `None` if not detected. 
     208    """ 
     209    match = re.search(MODE_RE, content[:1000]) 
     210    if match: 
     211        mode = match.group('prog1') 
     212        if mode and mode == 'env': 
     213            mode = match.group('prog2') 
     214        if not mode: 
     215            mode = match.group('vimsyntax') or \ 
     216                   match.group('emacsmode').lower() 
     217        if mode in mime_map: 
     218            # 3) mimetype from the content, using the `MODE_RE` 
     219            return mime_map[mode] 
     220    else: 
     221        if is_binary(content): 
     222            # 4) mimetype from the content, using `is_binary` 
     223            return APPLICATION_OCTET_STREAM_STR 
     224 
     225def get_mimetype(filename, content=None, mime_map=MIME_MAP): 
     226    """Auto-detect MIME type either from the `filename` or from the `content`. 
     227    """ 
     228    mimetype = get_mimetype_from_filename(filename, mime_map) 
     229    if not mimetype and content: 
     230        mimetype = get_mimetype_from_content(content, mime_map) 
     231    return mimetype 
     232 
    158233def is_binary(data): 
    159234    """Detect binary content by checking the first thousand bytes for zeroes. 
    160235 
     
    165240    return '\0' in data[:1000] 
    166241 
    167242def detect_unicode(data): 
    168     """Detect different unicode charsets by looking for BOMs (Byte Order Marks). 
     243    """Detect different unicode charsets by looking for Byte Order Marks. 
    169244 
    170245    Operate obviously only on `str` objects. 
    171246    """ 
     
    178253    else: 
    179254        return None 
    180255 
    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) 
    187256 
     257# -- ContentType and subclasses  
    188258 
    189 class IHTMLPreviewRenderer(Interface): 
    190     """Extension point interface for components that add HTML renderers of 
    191     specific content types to the `Mimeview` component. 
     259class ContentType(object): 
     260    """Abstract representation of the "type" of some content.""" 
    192261 
    193     (Deprecated) 
     262    def match(self, other): 
     263        """Compare this instance with another `ContentType` instance. 
     264 
     265        Return True if `other` can be say to ''match'' our type. 
     266        """ 
     267        raise NotImplementedError 
     268 
     269    is_binary = property(lambda x: x._is_binary()) 
     270 
     271    mimetype = property(lambda x: x._get_mimetype(), 
     272                        doc="MIME Type string (without charset information)") 
     273 
     274    name = 'unknown' 
     275 
     276 
     277class ObjectType(ContentType): 
     278    """Represent the "type" of a content by using a Python class. 
     279 
     280    The corresponding content is then expected to be an `ObjectContent`, 
     281    wrapping a Python instance of that class. 
    194282    """ 
    195283 
    196     # implementing classes should set this property to True if they 
    197     # support text content where Trac should expand tabs into spaces 
    198     expand_tabs = False 
     284    def __init__(self, obj): 
     285        self._class = isinstance(obj, type) and obj or obj.__class__ 
     286        self._mimetype = self._charset = None 
     287        self.name = self._class.__name__ 
    199288 
    200     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. 
     289    def __repr__(self): 
     290        return "<ObjectType %s>" % self.name 
     291 
     292    # Reimplemented methods 
     293     
     294    def _is_binary(self): return True 
     295    def _get_mimetype(self): return None 
     296    # or return 'application/x-python-'+self.name ? 
     297 
     298    def match(self, other, regexp=False): 
     299        other_class = isinstance(other, type) and other or \ 
     300                      isinstance(other, ObjectType) and other._class or \ 
     301                      other.__class__ 
     302        return other_class == self._class 
     303 
     304 
     305class MimeType(ContentType): 
     306    """Represent the "type" of a content by using a MIME type string. 
     307 
     308    If the type is not binary (i.e. is some kind of text), the `charset` 
     309    information can also be given, when creating the instance. 
     310 
     311    A MIME type has a `name` and has an `extension` that can be used 
     312    for storing the converted data in a file. 
     313 
     314    All the properties of this class are read-only. 
     315    """ 
     316     
     317    def __init__(self, mimetype, charset=None, name=None, extension=None, 
     318                 env=None): 
     319        """Create a MimeType based on a `mimetype` string. 
     320 
     321        That string can eventually contain a `charset=...`, which will be 
     322        retained as the charset for this instance, unless there's an 
     323        explicit `charset` parameter given. 
     324 
     325        Another possibility is to give a pattern object for the `mimetype` 
     326        argument, in which case this instance can be used to do pattern 
     327        matching when using `match()`. 
     328 
     329        When optional `env` parameter is given, additional knowledge is 
     330        used for determining whether this type is binary or not. 
    204331        """ 
     332        self.env = env 
     333        self._mimetype = mimetype 
     334        # determine charset 
     335        self._charset = charset 
     336        if not self._charset and isinstance(self._mimetype, basestring): 
     337            sep_idx = mimetype.find(';') 
     338            if sep_idx >= 0: 
     339                self._mimetype = mimetype[:sep_idx].strip() 
     340                charset_idx = mimetype.find('charset=', sep_idx) 
     341                if charset_idx >= 0: 
     342                    self._charset = mimetype[charset_idx+8:].strip() 
     343        if extension and not extension.startswith('.'): 
     344            extension = '.' + extension 
     345        self._extension = extension 
     346        self._name = name 
    205347 
    206     def render(req, mimetype, content, filename=None, url=None): 
    207         """Render an XHTML preview of the raw `content`. 
     348    def __repr__(self): 
     349        return '<MimeType "%s">' % self.mimetype_charset 
    208350 
    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 
     351    def _get_extension(self): 
     352        if self._extension is None: 
     353            self._extension = KNOWN_MIME_TYPES.get(self.mimetype, [''])[0] 
     354            if not self._extension: 
     355                detail = self.mimetype.split('/', 1)[1] 
     356                if detail.startswith('x-'): 
     357                    self._extension = detail[2:] 
     358            self._extension = '.' + self._extension 
     359        return self._extension 
    213360 
    214         It is assumed that the content will correspond to the given `mimetype`. 
     361    def _get_mimetype_charset(self): 
     362        """Combine in a single string the MIME type and charset information.""" 
     363        if self._mimetype and self._charset: 
     364            return '%s; charset=%s' % (self.mimetype, self.charset) 
     365        else: 
     366            return self.mimetype ### or default to utf-8 if not binary?  
     367     
     368    charset = property(lambda x: x._charset, 
     369                       doc="Charset information if available") 
    215370 
    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. 
     371    name = property(lambda x: x._name or x._get_extension()) 
     372    extension = property(lambda x: x._get_extension()) 
     373    mimetype_charset = property(lambda x: x._get_mimetype_charset(), 
     374                                doc="MIME Type plus charset information") 
     375 
     376    is_unknown = property(lambda x: x._mimetype is None) 
     377     
     378    # Reimplemented methods 
     379     
     380    def _is_binary(self): 
     381        return (self._mimetype == APPLICATION_OCTET_STREAM_STR or  
     382                (self.env and self.mimetype in 
     383                 Mimeview(self.env).treat_as_binary) or  
     384                self.is_unknown) 
     385 
     386    def _get_mimetype(self): 
     387        return self._mimetype or APPLICATION_OCTET_STREAM_STR 
     388 
     389    def match(self, other): 
     390        if isinstance(other, MimeType): 
     391            other = other.mimetype 
     392        if not isinstance(other, basestring): 
     393            return False 
     394        if hasattr(self.mimetype, 'match'): 
     395            return self.mimetype.match(other) 
     396        else: 
     397            return self.mimetype == other 
     398 
     399 
     400# A few commonly used MIME types: 
     401 
     402TEXT_PLAIN = MimeType('text/plain', 'utf-8', 'Plain Text', 'txt') 
     403TEXT_HTML = MimeType('text/html', 'utf-8', 'HTML', 'html') 
     404TEXT_CSV = MimeType('text/csv', 'utf-8', 'Comma-delimited Text', 'csv') 
     405TEXT_TSV = MimeType('text/tab-separated-values', 'utf-8', 
     406                    'Tab-delimited Text', 'tsv') 
     407 
     408APPLICATION_RSS_XML = MimeType('application/rss+xml', 'utf-8', 
     409                               'RSS Feed', 'xml') 
     410APPLICATION_OCTET_STREAM = MimeType(APPLICATION_OCTET_STREAM_STR, 
     411                                    IDENTITY_CHARSET, 
     412                                    'Undefined (binary)', 'bin')  
     413 
     414 
     415# -- AbstractContent and subclasses 
     416 
     417class AbstractContent(object): 
     418    """An abstract content, with an associated content type. 
     419 
     420    There can also be a `filename` eventually associated to this content. 
     421    """ 
     422 
     423    def __init__(self, env, content, type, filename=None): 
     424        self.env = env 
     425        self.content = content 
     426        self._type = type 
     427        self.filename = filename 
     428 
     429    def _is_binary(self): raise NotImplementedError 
     430 
     431    def _get_type(self): return self._type 
     432    def _set_type(self, type): self._type = type 
     433 
     434    # Properties 
     435     
     436    type = property(fget=lambda x: x._get_type(), 
     437                    fset=lambda x, y: x._set_type(y), doc= 
     438                    """The corresponding `ContentType` for this content.""") 
     439 
     440    is_binary = property(lambda x: x._is_binary(), doc= 
     441                         "True if content should be considered to be binary") 
     442     
     443    # Methods 
     444     
     445    def basename(self): 
     446        """Return a possible file basename for this content.""" 
     447        return self.filename and os.path.splitext(self.filename)[0] or '' 
     448 
     449    def convert(self, req, format=None, typespec=None, context=None): 
     450        """Convert this content to a `MimeContent`. 
     451 
     452        The converter can be selected by either: 
    220453         
    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. 
     454         - specifying the desired output `format`, which must match the 
     455           `format` of one of the `Conversion` object returned by 
     456           `Mimeview.get_conversions(self.type)`. 
     457           There's usually only one such converter. 
     458            
     459         - giving a `typespec`, which is a `MimeType` object. 
     460           All matching converters are tried in turn, until one succeeds. 
     461           This is quite flexible, as one can use a regexp match ### FIXME 
     462 
     463        Either succeeds or raise `NoConversion` error. 
    224464        """ 
     465        if not typespec and not format: 
     466            self.env.log.error("Convert called but no conversion specified.") 
     467            return self 
     468        if isinstance(format, MimeType): # or signal a programming error? 
     469            typespec = format 
     470            format = None 
    225471 
     472        # Get all available conversions for our `type`, and keep those 
     473        # which are matching either the `format` or the `typespec`. 
     474        mimeview = Mimeview(self.env) 
     475        candidates = [] 
     476        for conversion, converter in mimeview.get_converters(self.type): 
     477            if conversion.format == format or \ 
     478                   (typespec and typespec.match(conversion.output)): 
     479                candidates.append((conversion, converter)) 
     480        if not candidates: 
     481            raise NoConversion('No available MIME conversions', 
     482                               self.type, typespec, format) 
     483 
     484        tab_expanded = None # we don't want to expand tabs more than once. 
     485 
     486        # First candidate which converts successfully wins. 
     487        self.env.log.debug('Converting %s' % repr(self)) 
     488        if not context: 
     489            context = IContentConverter.ToplevelContext(req) 
     490        content = self 
     491        for conversion, converter in candidates: 
     492            self.env.log.debug('Trying converter %s using %s' % 
     493                               (repr(converter), repr(conversion))) 
     494            if conversion.expand_tabs and not tab_expanded: 
     495                expanded = unicode(content).expandtabs(mimeview.tab_width) 
     496                content = StringMimeContent(self.env, expanded, content.type, 
     497                                            filename=content.filename) 
     498                self.env.log.debug('(tab expansion performed)') 
     499            try: 
     500                result = converter.convert_content(context, conversion, content) 
     501                if result is not None: 
     502                    return result 
     503            except Exception, e: 
     504                self.env.log.warning('MIME conversion using %s failed (%s)' 
     505                                      % (repr(converter), e), exc_info=True) 
     506        raise NoConversion('No MIME conversions succeeded', 
     507                           content.type, typespec, format) 
     508 
     509 
     510class ObjectContent(AbstractContent): 
     511    """Content is a Python a object, and its type is an `ObjectType`. 
     512 
     513    Only supports the bare minimum of the `AbstractContent` methods. 
     514    `filename` in this case will be the suggested base name to be used 
     515    when creating a filename for a converted content. 
     516    """ 
     517 
     518    def __init__(self, env, obj, filename=None): 
     519        AbstractContent.__init__(self, env, obj, ObjectType(obj), 
     520                                 filename=filename) 
     521 
     522    # Reimplemented property readers 
     523 
     524    def _is_binary(self): 
     525        return True 
     526 
     527    def basename(self): 
     528        return AbstractContent.basename(self) or self.type.name 
     529 
     530 
     531class MimeContent(AbstractContent): 
     532    """Content associated with a MIME type. 
     533 
     534    Such an object has ways to auto-detect both the MIME Type and 
     535    the `encoding` of its content. 
     536 
     537    That `encoding` is more reliable than the `type.charset` information. 
     538    There are additional consistency checks that are performed, and it 
     539    can be `None` if the content is an `unicode` object. 
     540 
     541    The content itself can be accessed in various ways: through 
     542    the iterator protocol, the len() and unicode() operators... 
     543 
     544    This class may eventually be instantiated directly when we don't 
     545    care about the actual content but only about the MimeType detection. 
     546    """ 
     547 
     548    EXCERPT_LEN = 1000 
     549 
     550    disposition = 'inline' 
     551    last_modified = None # means unknown, otherwise is a time value 
     552 
     553    def __init__(self, env, content=None, mimetype=None, 
     554                 filename=None, url=None, annotable=True): 
     555        """ 
     556        `mimetype` can be specified as a `MimeType` object or as string, 
     557        which will then be used as a hint for the content. 
     558 
     559        If the mimetype is not specified or is "application/octet-stream", 
     560        then it will be auto-detected when needed. 
     561 
     562        In case auto-detection fails, the MIME type will be set 
     563        to APPLICATION_OCTET_STREAM. 
     564 
     565        The `filename` is the corresponding file name for that content, 
     566        or if there's none, a possible basename for that content. 
     567 
     568        The `url` is a link for retrieving the raw content directly 
     569        from the server. This can be useful for converters that can 
     570        provide links to objects instead of having to expand the content. 
     571        """ 
     572        if isinstance(mimetype, basestring): 
     573            mimetype = MimeType(mimetype, env=env) 
     574        AbstractContent.__init__(self, env, content, mimetype, filename) 
     575        self.url = url 
     576        self.annotable = annotable 
     577        self._binary = None 
     578        self._encoding = False # i.e. not yet determined 
     579        self._excerpt = None 
     580 
     581    def __repr__(self): 
     582        return '<%s %s "%s">' % (self.__class__.__name__, self._type, 
     583                                 self.filename or self.url) 
     584 
     585    # Reimplemented property accessors 
     586 
     587    def _is_binary(self): 
     588        """An heuristic for guessing whether the content is binary or not. 
     589 
     590        This will eventually fetch an `excerpt` of the content. 
     591        """ 
     592        if self._binary is None: 
     593            self._binary = self.type.is_binary 
     594            if not self._binary: # double-check using the content 
     595                self._binary = is_binary(self.excerpt) or \ 
     596                               (self.type.mimetype in \ 
     597                                Mimeview(self.env).treat_as_binary) 
     598        return self._binary 
     599 
     600    def _get_type(self): 
     601        """Get or determine the MimeType corresponding to this content. 
     602 
     603        An `excerpt` of the content will be examined if needed. 
     604        """ 
     605        if self._type is None: # not set 
     606            mimetype = None 
     607            if self.filename: 
     608                mimemap = Mimeview(self.env).mimemap 
     609                mimetype = get_mimetype_from_filename(self.filename, mimemap) 
     610            if not mimetype: 
     611                mimetype = get_mimetype_from_content(self.excerpt, mimemap) 
     612            if not mimetype: 
     613                pass # TODO 0.11: go through IMimeTypeDetectors 
     614            self._type = MimeType(mimetype, env=self.env) 
     615        return self._type 
     616 
     617    def _set_type(self, type): 
     618        """Simply replace the existing `type` by the given `MimeType` object. 
     619 
     620        If `None` is given, this will force auto-detection the next time 
     621        `type` will be accessed. 
     622 
     623        Can be used for in-place conversion (e.g. ''any'' to text/plain). 
     624        """ 
     625        self._type = type 
     626 
     627    # Helper methods 
     628 
     629    def _get_encoding(self): 
     630        """Get or determine the current encoding of that `content`. 
     631 
     632        The encoding will be determined using this order: 
     633         * from the charset information present in the mimetype information 
     634         * auto-detection of the charset from the `content` 
     635         * if nothing else worked, use the configured `default_charset` 
     636 
     637        If the `content` happens to be a genuine `unicode` object, then 
     638        this returns `None` (and this, whatever the `type.charset` says). 
     639        ### XXX 
     640         
     641        If the `content` is binary, then the encoding will be the identity 
     642        charset (ISO Latin 1). 
     643        """ 
     644        if self._encoding is False: 
     645            if isinstance(self.excerpt, unicode): 
     646                self._encoding = None 
     647            else: 
     648                charset = self.type.charset  
     649                if charset: 
     650                    # we have external knowledge about the charset, 
     651                    # this always override what we think it could be... 
     652                    self._encoding = charset 
     653                elif isinstance(self.excerpt, str): 
     654                    utf_encoding = detect_unicode(self.excerpt) 
     655                    if utf_encoding is not None: 
     656                        self._encoding = utf_encoding 
     657                    elif self.is_binary: 
     658                        self._encoding = IDENTITY_CHARSET 
     659                if self._encoding is False: 
     660                    pass # TODO 0.11: go through ICharsetDetectors here 
     661                if self._encoding is False: 
     662                    self._encoding = Mimeview(self.env).default_charset 
     663        return self._encoding 
     664     
     665    def _get_excerpt(self): 
     666        """Get once the initial content of the file.""" 
     667        if self._excerpt is None: 
     668            self._excerpt = self._retrieve_excerpt() 
     669        return self._excerpt 
     670             
     671    # Public API Methods 
     672 
     673    def __unicode__(self): 
     674        """Return the `unicode` object corresponding to the content. 
     675 
     676        Note: this should do the right thing if the content is already 
     677        `unicode`, i.e. it should be directly returned. 
     678        """ 
     679        return to_unicode(self.read(), self.encoding) 
     680 
     681    def encode(self, charset=None): 
     682        """Return a `str`, corresponding to the `charset` encoded content. 
     683 
     684        If `charset` is not specified or is the same as the original encoding, 
     685        the raw content is directly returned. 
     686        """ 
     687        if not charset or self.encoding == charset: 
     688            content = self.read() 
     689            if isinstance(content, unicode): 
     690                return content.encode('utf-8') 
     691            else: 
     692                return content 
     693        else: 
     694            return unicode(self).encode(charset) 
     695 
     696    def read(self): 
     697        """Should return a `basestring` containing the whole content. 
     698 
     699        This is either a `str` or an `unicode` object, depending on what's 
     700        most appropriate for the wrapped content. 
     701 
     702        A more specialized way to access the content is often more adequate 
     703        or efficient than this one and is of course '''absolutely''' required 
     704        when `chunck()` is implemented with `read()`... 
     705        """ 
     706        return ''.join(self.chunks()) 
     707 
     708    def chunks(self): 
     709        """Iterator on chunks of raw content or None if there's no raw content. 
     710        """ 
     711        return None 
     712 
     713    def lines(self): 
     714        """Iterator on lines of text content or None if there are no lines.""" 
     715        return None 
     716 
     717    def markup(self): 
     718        """Return content as Markup.""" 
     719        return None 
     720 
     721    def size(self): 
     722        """Size of the raw content, in bytes or `-1` if unknown beforehand.""" 
     723        return -1 
     724 
     725    def send(self, req, disposition=None): 
     726        """Send this content to the requester. 
     727 
     728        When specified, the `disposition` argument will override the content's 
     729        own disposition and will be used for the 'Content-Disposition' header. 
     730        If the 'filename=' info is not given, it will be automatically added 
     731        based on the already available information. 
     732 
     733        `send()` Never returns, as it raises `RequestDone` upon completion. 
     734        """ 
     735        from trac.web import RequestDone 
     736        req.send_response(200) 
     737        req.send_header('Content-Type', self.type.mimetype_charset) 
     738        if self.size >= 0: # size is known 
     739            req.send_header('Content-Length', self.size()) 
     740        if self.last_modified: 
     741            req.send_header('Last-Modified', http_date(self.last_modified)) 
     742        if not disposition: 
     743            disposition = self.disposition 
     744        if self.filename and 'filename=' not in disposition: 
     745            extension = os.path.splitext(self.filename)[1] 
     746            if not extension: 
     747                extension = self.type.extension 
     748            disposition += '; filename=' + self.basename() + extension 
     749        req.send_header('Content-Disposition', disposition) 
     750        req.end_headers() 
     751         
     752        if self.is_binary: 
     753            for chunk in self.chunks(): 
     754                req.write(chunk) 
     755        else: 
     756            req.write(self.encode()) 
     757        raise RequestDone 
     758 
     759    # Properties 
     760 
     761    encoding = property(lambda x: x._get_encoding()) 
     762    excerpt = property(lambda x: x._get_excerpt()) 
     763 
     764    # Methods that needs to be reimplemented by subclasses 
     765 
     766    def _retrieve_excerpt(self): 
     767        """Extracts an excerpts of the initial content of the file. 
     768 
     769        '''Note: Be sure to ''not'' use `self.encoding` while doing that, 
     770        as precisely an excerpt of the content is taken in that method.''' 
     771        """ 
     772        raise NotImplementedError 
     773 
     774 
     775class StringMimeContent(MimeContent): 
     776    """MIME-typed content wrapper for a basestring.""" 
     777 
     778    # Reimplemented MimeContent methods 
     779 
     780    def read(self): 
     781        return self.content 
     782     
     783    def chunks(self): 
     784        """Iterator on chunks of content.""" 
     785        buf = StringIO(self.read()) # can be used as we reimplement it 
     786        chunk = buf.read(1000) 
     787        while chunk: 
     788            yield chunk 
     789            chunk = buf.read(1000) 
     790 
     791    def lines(self): 
     792        """Iterator on lines.""" 
     793        for line in self.read().splitlines(): # don't keep eols? 
     794            yield line 
     795 
     796    def markup(self): 
     797        return Markup(self.read()) 
     798 
     799    def size(self): 
     800        """Length of the content, in characters.""" 
     801        return len(self.read()) 
     802     
     803    def _retrieve_excerpt(self): 
     804        return self.read()[:self.EXCERPT_LEN] 
     805 
     806 
     807class LineIteratorMimeContent(StringMimeContent): 
     808    """MIME-typed content wrapper for an iterable of lines.""" 
     809 
     810    # Reimplemented methods 
     811 
     812    def __unicode__(self): return u'\n'.join(self.content) 
     813    def read(self): return unicode(self) 
     814 
     815    def lines(self): 
     816        yield self.excerpt 
     817        for line in self.content: 
     818            yield line 
     819 
     820    def _retrieve_excerpt(self): 
     821        for first_line in self.content: 
     822            return first_line 
     823 
     824 
     825class StructuredMimeContent(StringMimeContent): 
     826    """MIME-typed content wrapper for an Element. 
     827 
     828    Nearly identical to the `StringMimeContent`, except the content 
     829    property will access to actual Element for structured processing. 
     830    """ 
     831 
     832    # Reimplemented methods 
     833 
     834    def read(self): return unicode(self.content) 
     835    def lines(self): return None 
     836    def markup(self): return self.content 
     837 
     838 
     839class FileMimeContent(MimeContent): 
     840    """MIME-typed content wrapper for a file.""" 
     841 
     842    def __init__(self, env, path, mimetype=None, url=None, kind='File'): 
     843        MimeContent.__init__(self, env, path, mimetype, 
     844                             os.path.basename(path), url) 
     845        self._fd = None 
     846        self._path = path 
     847        self._kind = kind 
     848 
     849    def __del__(self): 
     850        if self._fd: 
     851            self._fd.close() 
     852 
     853    # Reimplemented methods 
     854 
     855    def chunks(self): 
     856        """Iterate on chunks of raw content.""" 
     857        chunk = self.excerpt 
     858        while chunk: 
     859            yield chunk 
     860            chunk = self._fd.read(self.EXCERPT_LEN) 
     861        self._fd.seek(self.EXCERPT_LEN) # make it possible to iter again 
     862 
     863    def size(self): 
     864        """Length of the raw content, in bytes.""" 
     865        if self._fd: 
     866            stat = os.fstat(self._fd.fileno()) 
     867        else: 
     868            try: 
     869                stat = os.stat(self._path) 
     870            except OSError: 
     871                raise TracError('%s "%s" not found' % (self._kind, 
     872                                                       self.filename)) 
     873        return stat.st_size 
     874 
     875    def send(self, req, disposition=None): 
     876        """Directly send the file.""" 
     877        ### self.encoding should be used when charset not available 
     878        ### maybe do that in a self.mimetype_charset method  
     879        req.send_file(self._path, self.type.mimetype_charset) 
     880         
     881    def _retrieve_excerpt(self): 
     882        if not self._fd: 
     883            try: 
     884                self._fd = open(self._path) 
     885            except IOError: 
     886                raise TracError('%s "%s" not found' % (self._kind, 
     887                                                       self.filename)) 
     888        return self._fd.read(self.EXCERPT_LEN) 
     889 
     890 
     891# -- Conversion class and related 
     892 
     893class NoConversion(TracError): 
     894    def __init__(self, msg, from_, output, key): 
     895        TracError.__init__(self, '%s while converting from %s to %s' % 
     896                           (msg, repr(from_), output and repr(output) or key), 
     897                           'MIME Content Conversion') 
     898 
     899class Conversion(object): 
     900    """Used to specify how a Converter will perform a conversion. 
     901 
     902    Each conversion is identified by a `format`, which must be unique for 
     903    a given input `type` in `IContentConverter.get_supported_conversions()`. 
     904 
     905    A conversion object can tell: 
     906     - what will be the precise `output` MimeType (as opposed to the 
     907       specification given, which can be vague) 
     908     - what will be the `quality` of the conversion. This is a number 
     909       in the range 0 to 9, where 0 means no support and 9 means "perfect" 
     910       support (try to keep 9 available for user defined conversions, 
     911       though nothing will prevent them from using 10 or 100...) 
     912     - whether a tab expansion should precede the conversion itself, 
     913       (`expand_tabs` flag which defaults to False). 
     914 
     915    e.g. Conversion(format='latex', quality=8, type=MimeType('text/x-tex')) 
     916    """ 
     917 
     918    def __init__(self, format, quality=1, output=TEXT_HTML, expand_tabs=False): 
     919        self.format = format 
     920        self.quality = quality 
     921        self.output = output 
     922        self.expand_tabs = expand_tabs 
     923 
     924    def __repr__(self): 
     925        return '<Conversion %s %s (quality %s)>' % \ 
     926               (self.format, self.output, self.quality) 
     927 
     928 
     929# -- Interfaces for the extension points 
     930 
     931class IContentConverter(Interface): 
     932    """An extension point interface for generic content conversion.""" 
     933 
     934    def get_supported_conversions(input):  
     935        """Tells whether this converter can handle this `input` type. 
     936 
     937        Return an iterable of `Conversion` objects, each describing 
     938        how the conversion should be done and what will be the output type. 
     939        """ 
     940 
     941    def convert_content(context, conversion, content):  
     942        """Convert the given `AbstractContent` as specified by `Conversion`. 
     943 
     944        The conversion takes place in the given formatting `context`. 
     945        A `context` provides at least a `req` property. 
     946        If no other specific context object is available, a 
     947        `ToplevelContext` can be used to wrap the `req` instance. 
     948         
     949        Return the converted content, which ''must'' be a `MimeContent` object. 
     950        """  
     951    class ToplevelContext(object): 
     952        def __init__(self, req): 
     953            self.req = req 
     954 
     955 
    226956class IHTMLPreviewAnnotator(Interface): 
    227957    """Extension point interface for components that can annotate an XHTML 
    228958    representation of file contents with additional information.""" 
    229959 
    230960    def get_annotation_type(): 
    231         """Return a (type, label, description) tuple that defines the type of 
    232         annotation and provides human readable names. The `type` element should 
    233         be unique to the annotator. The `label` element is used as column 
    234         heading for the table, while `description` is used as a display name to 
    235         let the user toggle the appearance of the annotation type. 
     961        """Defines the type of annotation and provides human readable names. 
     962 
     963        Return a (type, label, description) tuple, where: 
     964         - `type` element should be unique to the annotator. 
     965         - `label` element is used as column heading for the table 
     966         - `description` is used as a display name to let the user toggle 
     967            the appearance of the annotation type. 
    236968        """ 
    237969 
    238970    def annotate_line(number, content): 
    239         """Return the XHTML markup for the table cell that contains the 
    240         annotation data.""" 
     971        """Return the annotation data for the given line. 
    241972 
     973        The return value must be XHTML markup that fit in a table cell. 
     974        """ 
    242975 
    243 class IContentConverter(Interface): 
    244     """An extension point interface for generic MIME based content 
    245     conversion.""" 
    246976 
    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)""" 
     977# -- The main Mimeview component 
    254978 
    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  
    261979class Mimeview(Component): 
    262980    """A generic class to prettify data, typically source code.""" 
    263981 
    264     renderers = ExtensionPoint(IHTMLPreviewRenderer) 
    265982    annotators = ExtensionPoint(IHTMLPreviewAnnotator) 
    266983    converters = ExtensionPoint(IContentConverter) 
    267984 
     
    275992        """Maximum file size for HTML preview. (''since 0.9'').""") 
    276993 
    277994    mime_map = ListOption('mimeviewer', 'mime_map', 
    278         'text/x-dylan:dylan,text/x-idl:ice,text/x-ada:ads:adb', 
     995        'text/x-dylan:dylan,text/x-idl:ice,text/x-ada:ads:adb', doc= 
    279996        """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'').""") 
    283997 
     998        Mappings are comma-separated. Each mapping starts with the mimetype, 
     999        followed by a colon (":") and the (colon separated) list of associated 
     1000        keywords or file extensions. (''since 0.10'').""") 
     1001 
     1002    treat_as_binary = ListOption('mimeviewer', 'treat_as_binary', 
     1003        'application/pdf,application/postscript,application/rtf', doc= 
     1004        """List of MIME types that should always be treated as binary content. 
     1005 
     1006        Accounts for the fact that our binary detection heuristic can't 
     1007        always work for some kind of binary data. (''since 0.10'').""") 
     1008 
    2841009    def __init__(self): 
    2851010        self._mime_map = None 
    286          
    287     # Public API 
    2881011 
    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.""" 
    293         converters = [] 
    294         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 
     1012    def _get_mimemap(self): 
     1013        """Extend default extension to MIME type mappings""" 
     1014        if not self._mime_map: 
     1015            self._mime_map = {} 
     1016            self._mime_map.update(MIME_MAP) 
     1017            for mapping in self.config['mimeviewer'].getlist('mime_map'): 
     1018                if ':' in mapping: 
     1019                    assocations = mapping.split(':') 
     1020                    mimetype = assocations[0] 
     1021                    for keyword in assocations:  
     1022                        self._mime_map[keyword] = mimetype 
     1023                    # Note: 'mimetype' is associated to itself 
     1024        return self._mime_map 
    3001025 
    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') 
     1026    mimemap = property(_get_mimemap) 
    3081027 
    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 
     1028    def get_mimetype(self, filename, charset=None): 
     1029        """Lookup for given `filename`, among known MIME Types. 
    3191030 
    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])] 
    323         if not candidates: 
    324             raise TracError('No available MIME conversions from %s to %s' % 
    325                             (mimetype, key)) 
     1031        `filename` is either a file name, or simply a keyword. 
    3261032 
    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)) 
     1033        Return a `MimeType` object if found, `None` otherwise. 
     1034        """ 
     1035        mimetype = get_mimetype_from_filename(filename, self.mimemap) 
     1036        if mimetype: 
     1037            ext = os.path.splitext(filename)[1] 
     1038            return MimeType(mimetype, charset, extension=ext, env=self.env) 
    3361039 
    337     def get_annotation_types(self): 
    338         """Generator that returns all available annotation types.""" 
    339         for annotator in self.annotators: 
    340             yield annotator.get_annotation_type() 
     1040    # -- MIME type conversion 
     1041     
     1042    def get_converters(self, input): 
     1043        """Return a list of conversions for the `input` `ContentType`. 
    3411044 
    342     def render(self, req, mimetype, content, filename=None, url=None, 
    343                annotations=None): 
    344         """Render an XHTML preview of the given `content`. 
     1045        The returned list contains pair of `(conversion, converter)` objects, 
     1046        ordered from best to worst quality. 
     1047        """ 
     1048        converters = [] 
     1049        for converter in self.converters: 
     1050            for conversion in converter.get_supported_conversions(input): 
     1051                if conversion.quality > 0: 
     1052                    converters.append((conversion, converter)) 
    3451053 
    346         `content` is the same as an `IHTMLPreviewRenderer.render`'s 
    347         `content` argument. 
     1054        return sorted(converters, key=lambda c: c[0].quality, reverse=True) 
    3481055 
    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. 
     1056    def get_conversions(self, input): 
     1057        """Convenience method, which return only the `Conversion` information. 
    3531058 
    354         Return a string containing the XHTML text. 
     1059        Otherwise, it's the same as `get_converters`. 
    3551060        """ 
    356         if not content: 
    357             return '' 
     1061        return [conversion for conversion, _ in self.get_converters(input)] 
    3581062 
    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 
     1063    # -- XHTML rendering and annotations (based on the conversion API) 
    3691064 
    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])) 
     1065    def render(self, context, content, annotations=None): 
     1066        """Render an XHTML preview of the given `content`. 
    3771067 
    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) 
     1068        Some `annotations` might be requested as well. This argument 
     1069        is a list of annotation keys, each of them should match a 
     1070        `type` as returned by `IHTMLPreviewAnnotator.get_annotation_type`. 
     1071        """ 
     1072        result = content.convert(context, typespec=TEXT_HTML) 
     1073        if annotations and result.annotable: 
     1074            lines = result.lines() 
     1075            if lines: 
     1076                return Markup(self._annotate(lines, annotations)) 
     1077            # i.e. don't annotate contents that are not line oriented 
     1078            ### don't annotate content which should not be annotated... 
    4131079 
     1080        if isinstance(result, LineIteratorMimeContent): 
     1081            return html.DIV(html.PRE(result.markup()), class_="code") 
     1082        else: 
     1083            return result.markup() 
     1084     
     1085    def get_annotation_types(self): 
     1086        """Generator that returns all available annotation types.""" 
     1087        for annotator in self.annotators: 
     1088            yield annotator.get_annotation_type() 
     1089 
    4141090    def _annotate(self, lines, annotations): 
     1091        """Add requested `annotations` to the lines' content.""" 
    4151092        buf = StringIO() 
    4161093        buf.write('<table class="code"><thead><tr>') 
    4171094        annotators = [] 
     
    4451122        buf.write('</tbody></table>') 
    4461123        return buf.getvalue() 
    4471124 
     1125    # -- Deprecated API (TODO: remove in 0.11) 
     1126 
    4481127    def get_max_preview_size(self): 
    4491128        """Deprecated: use `max_preview_size` attribute directly.""" 
    4501129        return self.max_preview_size 
    4511130 
    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  
    4981131    def to_utf8(self, content, mimetype=None): 
    4991132        """Convert an encoded `content` to utf-8. 
    5001133 
    5011134        ''Deprecated in 0.10. You should use `unicode` strings only.'' 
    5021135        """ 
    503         return to_utf8(content, self.get_charset(content, mimetype)) 
     1136        return to_utf8(content) 
    5041137 
    505     def to_unicode(self, content, mimetype=None, charset=None): 
    506         """Convert `content` (an encoded `str` object) to an `unicode` object. 
     1138    # -- Utilities 
    5071139 
    508         This calls `trac.util.to_unicode` with the `charset` provided, 
    509         or the one obtained by `Mimeview.get_charset()`. 
     1140    def configured_modes_mapping(self, renderer): 
     1141        """Utility for configurable custom converters 
     1142 
     1143        Return a MIME type to `(mode,quality)` mapping for given `option`, 
     1144        assuming a format of comma-separated <mimetype>:<mode>:<quality> 
     1145        associations. 
     1146 
     1147        See EnscriptConverter and SilverCityConverter. 
    5101148        """ 
    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`""" 
    5171149        types, option = {}, '%s_modes' % renderer 
    5181150        for mapping in self.config['mimeviewer'].getlist(option): 
    5191151            if not mapping: 
     
    5261158                                 "option." % (mapping, option)) 
    5271159        return types 
    5281160     
    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`. 
     1161    def preview_to_hdf(self, req, content, annotations=None): 
     1162        """Prepares a rendered preview to text/html of the given `content`. 
    5321163 
    533         Note: `content` will usually be an object with a `read` method. 
    534         """         
    535         if length >= self.max_preview_size: 
     1164        That content is eventually annotated with the specified `annotations`. 
     1165        """ 
     1166        self.log.debug("Rendering preview of %s" % repr(content)) 
     1167 
     1168        if content.size() >= self.max_preview_size: 
    5361169            return {'max_file_size_reached': True, 
    5371170                    'max_file_size': self.max_preview_size, 
    538                     'raw_href': url} 
     1171                    'raw_href': content.url} 
    5391172        else: 
    540             return {'preview': self.render(req, mimetype, content, filename, 
    541                                            url, annotations), 
    542                     'raw_href': url} 
     1173            try: 
     1174                preview = self.render(IContentConverter.ToplevelContext(req), 
     1175                                      content, annotations) 
     1176            except NoConversion, e: 
     1177                preview = None 
     1178            return {'preview': preview, 
     1179                    'raw_href': content.url} 
    5431180 
    544     def send_converted(self, req, in_type, content, selector, filename='file'): 
    545         """Helper method for converting `content` and sending it directly. 
    5461181 
    547         `selector` can be either a key or a MIME Type.""" 
    548         from trac.web import RequestDone 
    549         content, output_type, ext = self.convert_content(req, in_type, 
    550                                                          content, selector) 
    551         req.send_response(200) 
    552         req.send_header('Content-Type', output_type) 
    553         req.send_header('Content-Disposition', 'filename=%s.%s' % (filename, 
    554                                                                    ext)) 
    555         req.end_headers() 
    556         req.write(content) 
    557         raise RequestDone         
    558          
    5591182 
     1183# utility for Mimeview._annotate 
    5601184def _html_splitlines(lines): 
    5611185    """Tracks open and close tags in lines of HTML text and yields lines that 
    5621186    have no tags spanning more than one line.""" 
     
    6041228    def annotate_line(self, number, content): 
    6051229        return '<th id="L%s"><a href="#L%s">%s</a></th>' % (number, number, 
    6061230                                                            number) 
     1231#        return html.TH(html.A(number, href="#L%s" % number), id=number) 
    6071232 
    6081233 
    609 # -- Default renderers 
     1234# -- Default TEXT_HTML converters 
    6101235 
    6111236class 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. 
     1237    """Convert text to HTML-escaped text. 
     1238 
     1239    Will be used as a fallback for any kind of text for which no more 
     1240    specific HTML converter is available. 
    6141241    """ 
    615     implements(IHTMLPreviewRenderer) 
     1242    implements(IContentConverter) 
    6161243 
    617     expand_tabs = True 
     1244    def get_supported_conversions(self, input): 
     1245        if input.is_binary: 
     1246            return 
     1247        yield Conversion(format='default', ### TODO Conversion.output 
     1248                         quality=TEXT_PLAIN.match(input) and 8 or 1, 
     1249                         output=TEXT_HTML, expand_tabs=True) 
     1250         
     1251    def convert_content(self, context, conversion, content): 
     1252        if content.is_binary: 
     1253            self.log.debug("Binary data; can't be rendered as plain text.") 
     1254        else: 
     1255            if not TEXT_PLAIN.match(content.type): 
     1256                self.log.debug("Using plain text renderer as a fallback.") 
     1257            def escape_lines(): 
     1258                for line in content.lines(): 
     1259                    yield escape(line) 
     1260            plaintext_same_encoding = MimeType('text/plain', content.encoding) 
     1261            return LineIteratorMimeContent(self.env, escape_lines(),  
     1262                                           plaintext_same_encoding) 
    6181263 
    619     TREAT_AS_BINARY = [ 
    620         'application/pdf', 
    621         'application/postscript', 
    622         'application/rtf' 
    623     ] 
    6241264 
    625     def get_quality_ratio(self, mimetype): 
    626         if mimetype in self.TREAT_AS_BINARY: 
    627             return 0 
    628         return 1 
     1265class ImageRenderer(Component): 
     1266    """Inline image display. 
    6291267 
    630     def render(self, req, mimetype, content, filename=None, url=None): 
    631         if is_binary(content): 
    632             self.env.log.debug("Binary data; no preview available") 
    633             return 
     1268    This renderer doesn't need the actual data at all, only the url. 
     1269    """ 
    6341270 
    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) 
     1271    implements(IContentConverter) 
    6391272 
     1273    IMAGE_TYPE = MimeType(re.compile(r'image/')) 
    6401274 
    641 class ImageRenderer(Component): 
    642     """Inline image display. Here we don't need the `content` at all.""" 
    643     implements(IHTMLPreviewRenderer) 
     1275    def get_supported_conversions(self, input): 
     1276        if self.IMAGE_TYPE.match(input): 
     1277            yield Conversion(format='image', quality=8, output=TEXT_HTML) 
    6441278 
    645     def get_quality_ratio(self, mimetype): 
    646         if mimetype.startswith('image/'): 
    647             return 8 
    648         return 0 
     1279    def convert_content(self, context, conversion, content): 
     1280        if content.url: 
     1281            img = html.DIV(html.IMG(src=content.url, alt=content.filename), 
     1282                           class_="image-file") 
     1283            return StructuredMimeContent(self.env, img, TEXT_HTML) 
    6491284 
    650     def render(self, req, mimetype, content, filename=None, url=None): 
    651         if url: 
    652             return html.DIV(html.IMG(src=url,alt=filename), 
    653                             class_="image-file") 
    6541285 
     1286# ---- Backward compatibility support for IHTMLPreviewRenderer 
     1287# 
     1288# (TODO: remove in 0.11) 
     1289# 
    6551290 
    656 class WikiTextRenderer(Component): 
    657     """Render files containing Trac's own Wiki formatting markup.""" 
    658     implements(IHTMLPreviewRenderer) 
     1291class IHTMLPreviewRenderer(Interface): 
     1292    """Extension point interface for components that add HTML renderers of 
     1293    specific content types to the `Mimeview` component. 
    6591294 
    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 
     1295    Deprecated in 0.10, implement `IContentConverter` instead. 
     1296    """ 
    6641297 
    665     def render(self, req, mimetype, content, filename=None, url=None): 
    666         from trac.wiki import wiki_to_html 
    667         return wiki_to_html(content_to_unicode(self.env, content, mimetype), 
    668                             self.env, req) 
     1298    # implementing classes should set this property to True if they 
     1299    # support text content where Trac should expand tabs into spaces 
     1300    expand_tabs = False 
     1301 
     1302    def get_quality_ratio(mimetype): 
     1303        """Return the level of support this renderer provides""" 
     1304 
     1305    def render(req, mimetype, content, filename=None, url=None): 
     1306        """Render an XHTML preview of the raw `content`.""" 
     1307 
     1308 
     1309class PreviewRendererAdapter(Component): 
     1310    """Single IContentConverter which wraps legacy IHTMLPreviewRenderer""" 
     1311 
     1312    renderers = ExtensionPoint(IHTMLPreviewRenderer) 
     1313 
     1314    implements(IContentConverter) 
     1315 
     1316    # IContentConverter methods 
     1317 
     1318    def get_supported_conversions(self, input): 
     1319        if not isinstance(input, MimeType): 
     1320            return 
     1321        for renderer in self.renderers: 
     1322            qr = renderer.get_quality_ratio(input.mimetype) 
     1323            if qr > 0: 
     1324                expand_tabs = getattr(renderer, 'expand_tabs', False) 
     1325                yield Conversion(format=renderer.__class__.__name__, 
     1326                                 quality=qr, output=TEXT_HTML, 
     1327                                 expand_tabs=expand_tabs) 
     1328 
     1329    def convert_content(self, context, conversion, content): 
     1330        for renderer in self.renderers: 
     1331            if conversion.format == renderer.__class__.__name__: 
     1332                self.log.debug('Rendering using %s' % renderer) 
     1333                try: 
     1334                    result = renderer.render(context.req, 
     1335                                           content.type.mimetype, 
     1336                                           content, # ...which is read()able  
     1337                                           content.filename, content.url) 
     1338 
     1339                    if isinstance(result, Fragment): 
     1340                        return StructuredMimeContent(self.env, result) 
     1341                    elif isinstance(result, basestring): 
     1342                        self.log.warning('IHTMLPreviewRenderer: string %s' % 
     1343                                         result.__class__.__name__) 
     1344                        return StringMimeContent(self.env, result) 
     1345                    else: # something else... assume it's an iterable of lines 
     1346                        self.log.warning('IHTMLPreviewRenderer: type %s' % 
     1347                                         result.__class__.__name__) 
     1348                        result = LineIteratorMimeContent(self.env, result) 
     1349                        # Check if the conversion is really valid, by trying to 
     1350                        # access an excerpt of the result. This might trigger 
     1351                        # errors with legacy renderers returning an iterator... 
     1352                        check_result = result.excerpt 
     1353                        return result 
     1354                except Exception, e: 
     1355                    self.env.log.warning('HTML renderer %s failed (%s)' 
     1356                                          % (repr(renderer), e)) 
     1357                    self.env.log.debug('HTML renderer:',exc_info=True) 
  • trac/mimeview/silvercity.py

     
    2424 
    2525from trac.core import * 
    2626from trac.config import ListOption 
    27 from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 
     27from trac.mimeview.api import IContentConverter, Mimeview, StringMimeContent, \ 
     28                              Conversion, TEXT_HTML 
    2829 
    2930__all__ = ['SilverCityRenderer'] 
    3031 
     
    5758CRLF_RE = re.compile('\r$', re.MULTILINE) 
    5859 
    5960 
    60 class SilverCityRenderer(Component): 
     61class SilverCityConverter(Component): 
    6162    """Syntax highlighting based on SilverCity.""" 
    6263 
    63     implements(IHTMLPreviewRenderer) 
     64    implements(IContentConverter) 
    6465 
    65     enscript_modes = ListOption('mimeviewer', 'silvercity_modes', 
    66         '', 
     66    silvercity_modes = ListOption('mimeviewer', 'silvercity_modes', doc= 
    6767        """List of additional MIME types known by SilverCity. 
     68         
    6869        For each, a tuple `mimetype:mode:quality` has to be 
    6970        specified, where `mimetype` is the MIME type, 
    7071        `mode` is the corresponding SilverCity mode to be used 
     
    7980    def __init__(self): 
    8081        self._types = None 
    8182 
    82     def get_quality_ratio(self, mimetype): 
     83    def get_supported_conversions(self, input): 
    8384        # Extend default MIME type to mode mappings with configured ones 
    8485        if not self._types: 
    8586            self._types = {} 
    8687            self._types.update(types) 
    87             self._types.update( 
    88                 Mimeview(self.env).configured_modes_mapping('silvercity')) 
    89         return self._types.get(mimetype, (None, 0))[1] 
     88            ctypes = Mimeview(self.env).configured_modes_mapping('silvercity') 
     89            self._types.update(ctypes) 
     90        quality_ratio = self._types.get(input.mimetype, (None, 0))[1] 
     91        if quality_ratio: 
     92            yield Conversion('silvercity', quality_ratio, TEXT_HTML) 
    9093 
    91     def render(self, req, mimetype, content, filename=None, rev=None): 
     94    def convert_content(self, context, conversion, input): 
    9295        import SilverCity 
    9396        try: 
    94             mimetype = mimetype.split(';', 1)[0] 
    95             typelang = self._types[mimetype] 
     97            typelang = self._types[input.type.mimetype] 
    9698            lang = typelang[0] 
    9799            module = getattr(SilverCity, lang) 
    98100            generator = getattr(module, lang + "HTMLGenerator") 
     
    108110            raise Exception, err 
    109111 
    110112        # SilverCity does not like unicode strings 
    111         content = content.encode('utf-8') 
     113        content = input.encode('utf-8') 
    112114         
    113115        # SilverCity generates extra empty line against some types of 
    114116        # the line such as comment or #include with CRLF. So we 
     
    122124        span_default_re = re.compile(r'<span class="\w+_default">(.*?)</span>', 
    123125                                     re.DOTALL) 
    124126        html = span_default_re.sub(r'\1', br_re.sub('', buf.getvalue())) 
    125          
    126         # Convert the output back to a unicode string 
    127         html = html.decode('utf-8') 
    128127 
    129128        # SilverCity generates _way_ too many non-breaking spaces... 
    130129        # We don't need them anyway, so replace them by normal spaces 
    131         return html.replace('&nbsp;', ' ').splitlines() 
     130        return StringMimeContent(self.env, html.replace('&nbsp;', ' '), 
     131                                 TEXT_HTML) 
  • 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.html 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.html 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/ticket/web_ui.py

     
    2323from trac.config import BoolOption, Option 
    2424from trac.core import * 
    2525from trac.env import IEnvironmentSetupParticipant 
     26from trac.mimeview.api import * 
    2627from trac.ticket import Milestone, Ticket, TicketSystem, ITicketManipulator 
    2728from trac.ticket.notification import TicketNotifyEmail 
    2829from trac.Timeline import ITimelineEventProvider 
     
    3334from trac.web import IRequestHandler 
    3435from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 
    3536from trac.wiki import wiki_to_html, wiki_to_oneliner 
    36 from trac.mimeview.api import Mimeview, IContentConverter 
    3737 
    3838 
    3939class InvalidTicket(TracError): 
     
    211211 
    212212    # IContentConverter methods 
    213213 
    214     def get_supported_conversions(self): 
    215         yield ('csv', 'Comma-delimited Text', 'csv', 
    216                'trac.ticket.Ticket', 'text/csv', 8) 
    217         yield ('tab', 'Tab-delimited Text', 'tsv', 
    218                'trac.ticket.Ticket', 'text/tab-separated-values', 8) 
    219         yield ('rss', 'RSS Feed', 'xml', 
    220                'trac.ticket.Ticket', 'application/rss+xml', 8) 
     214    def get_supported_conversions(self, input): 
     215        if input.match(Ticket): 
     216            yield Conversion('csv', 8, TEXT_CSV) 
     217            yield Conversion('tab', 8, TEXT_TSV) 
     218            yield Conversion('rss', 8, APPLICATION_RSS_XML) 
    221219 
    222     def convert_content(self, req, mimetype, ticket, key): 
     220    def convert_content(self, context, conversion, content): 
     221        ticket = content.content 
     222        key = conversion.format 
    223223        if key == 'csv': 
    224             return self.export_csv(ticket, mimetype='text/csv') 
     224            data = self.export_csv(ticket) 
    225225        elif key == 'tab': 
    226             return self.export_csv(ticket, sep='\t', 
    227                                    mimetype='text/tab-separated-values') 
     226            data = self.export_csv(ticket, sep='\t') 
    228227        elif key == 'rss': 
    229             return self.export_rss(req, ticket) 
     228            data = self.export_rss(context.req, ticket) 
     229        return StringMimeContent(self.env, data, conversion.output, 
     230                                 filename=content.basename()) 
    230231 
    231232    # INavigationContributor methods 
    232233 
     
    254255 
    255256        ticket = Ticket(self.env, id, db=db) 
    256257 
     258        # Send as alternate content if required 
     259        format = req.args.get('format') 
     260        if format: 
     261            content = ObjectContent(self.env, ticket, 
     262                                    filename='ticket_%d' % ticket.id) 
     263            content.convert(req, format).send(req) 
     264 
    257265        if req.method == 'POST': 
    258266            if not req.args.has_key('preview'): 
    259267                self._do_save(req, db, ticket) 
     
    281289        self._insert_ticket_data(req, db, ticket, 
    282290                                 get_reporter_id(req, 'author')) 
    283291 
    284         mime = Mimeview(self.env) 
    285         format = req.args.get('format') 
    286         if format: 
    287             mime.send_converted(req, 'trac.ticket.Ticket', ticket, format, 
    288                                 'ticket_%d' % ticket.id) 
    289  
    290292        # If the ticket is being shown in the context of a query, add 
    291293        # links to help navigate in the query result set 
    292294        if 'query_tickets' in req.session: 
     
    308310        add_stylesheet(req, 'common/css/ticket.css') 
    309311 
    310312        # Add registered converters 
    311         for conversion in mime.get_supported_conversions('trac.ticket.Ticket'): 
    312             conversion_href = req.href.ticket(ticket.id, format=conversion[0]) 
    313             add_link(req, 'alternate', conversion_href, conversion[1], 
    314                      conversion[3]) 
     313        for c in Mimeview(self.env).get_conversions(ObjectType(Ticket)): 
     314            add_link(req, 'alternate', 
     315                     req.href.ticket(ticket.id, format=c.format), 
     316                     c.output.name, c.output.mimetype) 
    315317 
    316318        return 'ticket.cs', None 
    317319 
     
    429431 
    430432    # Internal methods 
    431433 
    432     def export_csv(self, ticket, sep=',', mimetype='text/plain'): 
    433         content = StringIO() 
    434         content.write(sep.join(['id'] + [f['name'] for f in ticket.fields]) 
    435                       + CRLF) 
    436         content.write(sep.join([unicode(ticket.id)] + 
    437                                 [ticket.values.get(f['name'], '') 
    438                                  .replace(sep, '_').replace('\\', '\\\\') 
    439                                  .replace('\n', '\\n').replace('\r', '\\r') 
    440                                  for f in ticket.fields]) + CRLF) 
    441         return (content.getvalue(), '%s;charset=utf-8' % mimetype) 
    442          
     434    def export_csv(self, ticket, sep=','): 
     435        csv = StringIO() 
     436        csv.write(sep.join(['id'] + [f['name'] for f in ticket.fields]) + CRLF) 
     437        csv.write(sep.join([unicode(ticket.id)] + 
     438                           [ticket.values.get(f['name'], '') 
     439                            .replace(sep, '_').replace('\\', '\\\\') 
     440                            .replace('\n', '\\n').replace('\r', '\\r') 
     441                            for f in ticket.fields]) + CRLF) 
     442        return csv.getvalue() 
     443 
    443444    def export_rss(self, req, ticket): 
    444445        db = self.env.get_db_cnx() 
    445446        changes = [] 
     
    471472            change['title'] = '; '.join(['%s %s' % (', '.join(v), k) for k, v \ 
    472473                                         in change_summary.iteritems()]) 
    473474        req.hdf['ticket.changes'] = changes 
    474         return (req.hdf.render('ticket_rss.cs'), 'application/rss+xml') 
     475        return req.hdf.render('ticket_rss.cs') 
    475476 
    476  
    477477    def _do_save(self, req, db, ticket): 
    478478        if req.perm.has_permission('TICKET_CHGPROP'): 
    479479            # TICKET_CHGPROP gives permission to edit the ticket 
  • trac/ticket/tests/conversion.py

     
    11from trac.test import EnvironmentStub, Mock 
    2 from trac.util import sorted 
     2from trac.util import sorted, CRLF 
    33from trac.ticket.model import Ticket 
    44from trac.ticket.web_ui import TicketModule 
    5 from trac.mimeview.api import Mimeview 
     5from trac.mimeview.api import * 
    66from trac.web.clearsilver import HDFWrapper 
    77from trac.web.href import Href 
    88 
     
    1414    def setUp(self): 
    1515        self.env = EnvironmentStub() 
    1616        self.ticket_module = TicketModule(self.env) 
    17         self.mimeview = Mimeview(self.env) 
    1817        self.req = Mock(hdf=HDFWrapper(['./templates']), 
    1918                        base_path='/trac.cgi', path_info='', 
    20                         href=Href('/trac.cgi')) 
     19                        href=Href('/trac.cgi'), 
     20                        abs_href=Href('http://example.org/trac.cgi')) 
    2121 
    22     def _create_a_ticket(self): 
     22    def _create_a_ticket_content(self): 
    2323        # 1. Creating ticket 
    2424        ticket = Ticket(self.env) 
    2525        ticket['reporter'] = 'santa' 
    2626        ticket['summary'] = 'Foo' 
    2727        ticket['description'] = 'Bar' 
    2828        ticket['foo'] = 'This is a custom field' 
    29         return ticket 
     29        # 2. Creating ObjectContent wrapper 
     30        return ObjectContent(self.env, ticket) 
    3031 
    3132    def test_conversions(self): 
    32         conversions = self.mimeview.get_supported_conversions( 
    33             'trac.ticket.Ticket') 
    34         expected = sorted([('csv', 'Comma-delimited Text', 'csv', 
    35                            'trac.ticket.Ticket', 'text/csv', 8, 
    36                            self.ticket_module), 
    37                           ('tab', 'Tab-delimited Text', 'tsv', 
    38                            'trac.ticket.Ticket', 'text/tab-separated-values', 8, 
    39                            self.ticket_module), 
    40                            ('rss', 'RSS Feed', 'xml', 
    41                             'trac.ticket.Ticket', 'application/rss+xml', 8, 
    42                             self.ticket_module)], 
    43                           key=lambda i: i[-1], reverse=True) 
    44         self.assertEqual(expected, conversions) 
     33        conversions = Mimeview(self.env).get_conversions(ObjectType(Ticket)) 
     34        expected = [Conversion('csv', 8, TEXT_CSV), 
     35                           Conversion('rss', 8, APPLICATION_RSS_XML), 
     36                           Conversion('tab', 8, TEXT_TSV)] 
     37        for expected, actual in zip(expected, sorted(conversions, 
     38                                                     key=lambda c: c.format)): 
     39            for attr in ('format', 'output', 'quality', 'expand_tabs'): 
     40                self.assertEqual(getattr(expected, attr), 
     41                                 getattr(actual, attr)) 
    4542 
    4643    def test_csv_conversion(self): 
    47         ticket = self._create_a_ticket() 
    48         csv = self.mimeview.convert_content(self.req, 'trac.ticket.Ticket', 
    49                                             ticket, 'csv') 
    50         self.assertEqual((u'id,summary,reporter,owner,description,keywords,cc' 
    51                           '\r\nNone,Foo,santa,,Bar,,\r\n', 
    52                           'text/csv;charset=utf-8', 'csv'), csv) 
     44        content = self._create_a_ticket_content() 
     45        expected = [u'id,summary,reporter,owner,description,keywords,cc', 
     46                    u'None,Foo,santa,,Bar,,', 
     47                    ''] 
    5348 
     49        csv = content.convert(self.req, 'csv') 
     50        self.assertEqual(CRLF.join(expected), csv.read()) 
     51        self.assertEqual(CRLF.join(expected), unicode(csv)) 
     52        self.assertEqual(TEXT_CSV, csv.type) 
     53        self.assertEqual('text/csv; charset=utf-8', csv.type.mimetype_charset) 
    5454 
     55        csv = content.convert(self.req, TEXT_CSV) 
     56        self.assertEqual(CRLF.join(expected), csv.read()) 
     57        self.assertEqual(CRLF.join(expected), unicode(csv)) 
     58        self.assertEqual(TEXT_CSV, csv.type) 
     59        self.assertEqual('text/csv; charset=utf-8', csv.type.mimetype_charset) 
     60 
     61 
    5562    def test_tab_conversion(self): 
    56         ticket = self._create_a_ticket() 
    57         csv = self.mimeview.convert_content(self.req, 'trac.ticket.Ticket', 
    58                                             ticket, 'tab') 
    59         self.assertEqual((u'id\tsummary\treporter\towner\tdescription\tkeywords' 
    60                           '\tcc\r\nNone\tFoo\tsanta\t\tBar\t\t\r\n', 
    61                           'text/tab-separated-values;charset=utf-8', 'tsv'), 
    62                          csv) 
     63        content = self._create_a_ticket_content() 
     64        expected = [u'id\tsummary\treporter\towner\tdescription\tkeywords\tcc', 
     65                    u'None\tFoo\tsanta\t\tBar\t\t', 
     66                    ''] 
    6367 
     68        tsv = content.convert(self.req, 'tab') 
     69        self.assertEqual(CRLF.join(expected), tsv.read()) 
     70        self.assertEqual(CRLF.join(expected), unicode(tsv)) 
     71        self.assertEqual(TEXT_TSV, tsv.type) 
     72        self.assertEqual('text/tab-separated-values; charset=utf-8', 
     73                         tsv.type.mimetype_charset) 
     74 
     75        tsv = content.convert(self.req, TEXT_TSV) 
     76        self.assertEqual(CRLF.join(expected), tsv.read()) 
     77        self.assertEqual(CRLF.join(expected), unicode(tsv)) 
     78        self.assertEqual(TEXT_TSV, tsv.type) 
     79        self.assertEqual('text/tab-separated-values; charset=utf-8', 
     80                         tsv.type.mimetype_charset) 
     81 
    6482    def test_rss_conversion(self): 
    65         ticket = self._create_a_ticket() 
    66         content, mimetype, ext = self.mimeview.convert_content( 
    67             self.req, 'trac.ticket.Ticket', ticket, 'rss') 
    68         self.assertEqual(('<?xml version="1.0"?>\n<!-- RSS generated by Trac v ' 
    69                           'on  -->\n<rss version="2.0">\n <channel>\n   ' 
    70                           '<title>Ticket </title>\n  <link></link>\n  ' 
    71                           '<description>&lt;p&gt;\nBar\n&lt;/p&gt;\n' 
    72                           '</description>\n  <language>en-us</language>\n  ' 
    73                           '<generator>Trac v</generator>\n </channel>\n</rss>\n', 
    74                           'application/rss+xml', 'xml'), 
    75                          (content.replace('\r', ''), mimetype, ext)) 
     83        content = self._create_a_ticket_content() 
     84        expected = ['<?xml version="1.0"?>', 
     85                    '<!-- RSS generated by Trac v on  -->', 
     86                    '<rss version="2.0">', 
     87                    ' <channel>', 
     88                    '   <title>Ticket </title>', 
     89                    '  <link></link>', 
     90                    '  <description>&lt;p&gt;\r', 
     91                    'Bar\r', 
     92                    '&lt;/p&gt;\r', 
     93                    '</description>', 
     94                    '  <language>en-us</language>', 
     95                    '  <generator>Trac v</generator>', 
     96                    ' </channel>', 
     97                    '</rss>', 
     98                    ''] 
    7699 
     100        rss = content.convert(self.req, 'rss') 
     101        self.assertEqual('\n'.join(expected), rss.read()) 
     102        self.assertEqual('\n'.join(expected), unicode(rss)) 
     103        self.assertEqual(APPLICATION_RSS_XML, rss.type) 
     104        self.assertEqual('application/rss+xml; charset=utf-8', 
     105                         rss.type.mimetype_charset) 
    77106 
     107        rss = content.convert(self.req, APPLICATION_RSS_XML) 
     108        self.assertEqual('\n'.join(expected), rss.read()) 
     109        self.assertEqual('\n'.join(expected), unicode(rss)) 
     110        self.assertEqual(APPLICATION_RSS_XML, rss.type) 
     111        self.assertEqual('application/rss+xml; charset=utf-8', 
     112                         rss.type.mimetype_charset) 
     113 
    78114def suite(): 
    79115    return unittest.makeSuite(TicketConversionTestCase, 'test') 
    80116 
  • trac/ticket/roadmap.py

     
    184184 
    185185    # Internal methods 
    186186 
    187     def render_ics(self, req, db, milestones): 
     187    def render_ics(self, req, db, milestones): # FIXME: use IContentConverter 
    188188        req.send_response(200) 
    189189        req.send_header('Content-Type', 'text/calendar;charset=utf-8') 
    190190        req.end_headers() 
  • trac/ticket/query.py

     
    2121 
    2222from trac.core import * 
    2323from trac.db import get_column_names 
     24from trac.mimeview.api import * 
    2425from trac.perm import IPermissionRequestor 
    2526from trac.ticket import Ticket, TicketSystem 
    2627from trac.util.datefmt import format_datetime, http_date 
     
    3132                            INavigationContributor 
    3233from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider 
    3334from trac.wiki.macros import WikiMacroBase 
    34 from trac.mimeview.api import Mimeview, IContentConverter 
    3535 
    3636class QuerySyntaxError(Exception): 
    3737    """Exception raised when a ticket query cannot be parsed from a string.""" 
     
    351351               IContentConverter) 
    352352 
    353353    # IContentConverter methods 
    354     def get_supported_conversions(self): 
    355         yield ('rss', 'RSS Feed', 'xml', 
    356                'trac.ticket.Query', 'application/rss+xml', 8) 
    357         yield ('csv', 'Comma-delimited Text', 'csv', 
    358                'trac.ticket.Query', 'text/csv', 8) 
    359         yield ('tab', 'Tab-delimited Text', 'tsv', 
    360                'trac.ticket.Query', 'text/tab-separated-values', 8) 
     354    def get_supported_conversions(self, input): 
     355        if input.match(Query): 
     356            yield Conversion('rss', 8, APPLICATION_RSS_XML) 
     357            yield Conversion('csv', 8, TEXT_CSV) 
     358            yield Conversion('tab', 8, TEXT_TSV) 
    361359 
    362     def convert_content(self, req, mimetype, query, key): 
     360    def convert_content(self, context, conversion, content): 
     361        query = content.content 
     362        req = context.req 
     363        key = conversion.format 
    363364        if key == 'rss': 
    364             return self.export_rss(req, query) 
    365         elif key == 'csv': 
    366             return self.export_csv(req, query, mimetype='text/csv') 
     365            data = self.export_rss(req, query) 
    367366        elif key == 'tab': 
    368             return self.export_csv(req, query, '\t', 'text/tab-separated-values') 
     367            data = self.export_csv(req, query, '\t') 
     368        else: # key == 'tab': 
     369            data = self.export_csv(req, query) 
     370        return StringMimeContent(self.env, data, conversion.output, 
     371                                 filename=content.basename()) 
    369372 
    370373    # INavigationContributor methods 
    371374 
     
    413416                    del req.session[var] 
    414417            req.redirect(query.get_href(req)) 
    415418 
    416         # Add registered converters 
    417         for conversion in Mimeview(self.env).get_supported_conversions( 
    418                                              'trac.ticket.Query'): 
    419             add_link(req, 'alternate', 
    420                      query.get_href(req, format=conversion[0]), 
    421                      conversion[1], conversion[3]) 
     419        # Send as alternate content if required 
     420        format = req.args.get('format') 
     421        if format: 
     422            ObjectContent(self.env, query).convert(req, format).send(req) 
     423            # quite Rubyesque, isn't it? ;) 
    422424 
    423425        constraints = {} 
    424426        for k, v in query.constraints.items(): 
     
    435437            constraints[k] = constraint 
    436438        req.hdf['query.constraints'] = constraints 
    437439 
    438         format = req.args.get('format') 
    439         if format: 
    440             Mimeview(self.env).send_converted(req, 'trac.ticket.Query', query, 
    441                                               format, 'query') 
    442  
    443440        self.display_html(req, query) 
    444441        return 'query.cs', None 
    445442 
     
    615612           self.env.is_component_enabled(ReportModule): 
    616613            req.hdf['query.report_href'] = req.href.report() 
    617614 
    618     def export_csv(self, req, query, sep=',', mimetype='text/plain'): 
    619         content = StringIO() 
     615        # Add registered converters 
     616        for c in Mimeview(self.env).get_conversions(ObjectType(query)): 
     617            add_link(req, 'alternate', query.get_href(req, format=c.format), 
     618                     c.output.name, c.output.mimetype) 
     619 
     620    def export_csv(self, req, query, sep=','): 
     621        csv = StringIO() 
    620622        cols = query.get_columns() 
    621         content.write(sep.join([col for col in cols]) + CRLF) 
     623        csv.write(sep.join([col for col in cols]) + CRLF) 
    622624 
    623         results = query.execute(req, self.env.get_db_cnx()) 
    624         for result in results: 
    625             content.write(sep.join([unicode(result[col]).replace(sep, '_') 
    626                                                         .replace('\n', ' ') 
    627                                                         .replace('\r', ' ') 
     625        lines = query.execute(req, self.env.get_db_cnx()) 
     626        for line in lines: 
     627            csv.write(sep.join([unicode(line[col]).replace(sep, '_') 
     628                                                  .replace('\n', ' ') 
     629                                                  .replace('\r', ' ') 
    628630                                    for col in cols]) + CRLF) 
    629         return (content.getvalue(), '%s;charset=utf-8' % mimetype) 
     631        return csv.getvalue() 
    630632 
    631633    def export_rss(self, req, query): 
    632634        query.verbose = True 
     
    648650                groupdesc=query.groupdesc and 1 or None, 
    649651                verbose=query.verbose and 1 or None, 
    650652                **query.constraints) 
    651         return (req.hdf.render('query_rss.cs'), 'application/rss+xml') 
     653        return req.hdf.render('query_rss.cs') 
    652654 
    653655    # IWikiSyntaxProvider methods 
    654656     
  • 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 MimeContent 
    2325from trac.util.datefmt import format_datetime, pretty_timedelta 
    2426from trac.util.html import escape, html, Markup 
    2527from trac.util.text import shorten_line 
     
    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(MimeContent): 
     45    """MimeContent wrapper for a file Node in the repository.""" 
     46             
     47    def __init__(self, env, node, url=None): 
     48        MimeContent.__init__(self, env, node, node.content_type, 
     49                             posixpath.basename(node.path), url) 
     50        self.last_modified = node.last_modified 
     51        self._stream = None         
     52 
     53    def chunks(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._stream.read(CHUNK_SIZE) 
     62 
     63    def size(self): 
     64        return self.content.get_content_length() 
     65 
     66    def _retrieve_excerpt(self): 
     67        self._stream = self.content.get_content() 
     68        return self._stream.read(CHUNK_SIZE) 
     69             
     70 
    3271def get_changes(env, repos, revs, full=None, req=None, format=None): 
    3372    db = env.get_db_cnx() 
    3473    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_node) 
     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) 
     451             
     452            # We could theoretically avoid converting to unicode() in case 
     453            # both files share the same encoding, i.e. 
     454            # 
     455            #   old_content.type.encoding == new_content.type.encoding 
     456            # 
     457            # however it's not safe to call splitlines on arbitrary str 
     458            # see r3236. A workaround could be to use re.split(r'[\r\n]+') ? 
    453459 
    454460            if old_content != new_content: 
    455461                context = 3 
     
    526532                        'filename=%s.diff' % filename) 
    527533        req.end_headers() 
    528534 
    529         mimeview = Mimeview(self.env) 
    530535        for old_node, new_node, kind, change in repos.get_changes(**diff): 
    531536            # TODO: Property changes 
    532537 
     
    536541 
    537542            new_content = old_content = '' 
    538543            new_node_info = old_node_info = ('','') 
    539             mimeview = Mimeview(self.env) 
    540544 
    541545            if old_node: 
    542                 old_content = old_node.get_content().read() 
    543                 if is_binary(old_content): 
     546                old_content = NodeMimeContent(self.env, old_node) 
     547                if old_content.is_binary: 
    544548                    continue 
    545549                old_node_info = (old_node.path, old_node.rev) 
    546                 old_content = mimeview.to_unicode(old_content, 
    547                                                   old_node.content_type) 
     550                old_content = unicode(old_content) 
    548551            if new_node: 
    549                 new_content = new_node.get_content().read() 
    550                 if is_binary(new_content): 
     552                new_content = NodeMimeContent(self.env, new_node) 
     553                if new_content.is_binary: 
    551554                    continue 
    552555                new_node_info = (new_node.path, new_node.rev) 
    553556                new_path = new_node.path 
    554                 new_content = mimeview.to_unicode(new_content, 
    555                                                   new_node.content_type) 
     557                new_content = unicode(new_content) 
    556558            else: 
    557559                old_node_path = repos.normalize_path(old_node.path) 
    558560                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, TEXT_PLAIN 
    2727from trac.perm import IPermissionRequestor 
    2828from trac.util import sorted, embedded_numbers 
    29 from trac.util.datefmt import http_date, format_datetime, pretty_timedelta 
     29from trac.util.datefmt import format_datetime, pretty_timedelta 
    3030from trac.util.html import escape, html, Markup 
    3131from trac.util.text import pretty_size 
    3232from trac.web import IRequestHandler, RequestDone 
     
    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) 
    203  
    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' 
    211  
     199        content = NodeMimeContent(self.env, node, 
     200                                  req.href.browser(node.path, rev=rev, 
     201                                                   format='raw')) 
    212202        # Eventually send the file directly 
    213203        format = req.args.get('format') 
    214         if format in ['raw', 'txt']: 
    215             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) 
    219             req.send_header('Last-Modified', http_date(node.last_modified)) 
    220             req.end_headers() 
    221  
    222             while 1: 
    223                 if not chunk: 
    224                     raise RequestDone 
    225                 req.write(chunk) 
    226                 chunk = content.read(CHUNK_SIZE) 
     204        if format in ('raw', 'txt'): 
     205            if format == 'txt': 
     206                content.type = TEXT_PLAIN ### SHOULD BE ENOUGH 
     207                content.type = MimeType('text/plain', content.encoding) 
     208            content.send(req) 
    227209        else: 
    228210            # The changeset corresponding to the last change on `node`  
    229211            # is more interesting than the `rev` changeset. 
    230             changeset = repos.get_changeset(node.rev) 
     212            changeset = repos.get_changeset(node.created_rev) 
    231213 
    232214            message = changeset.message or '--' 
    233215            if self.config['changeset'].getbool('wiki_format_messages'): 
     
    238220 
    239221            req.hdf['file'] = { 
    240222                'rev': node.rev, 
    241                 'changeset_href': req.href.changeset(node.rev), 
     223                'changeset_href': req.href.changeset(node.created_rev), 
    242224                'date': format_datetime(changeset.date), 
    243225                'age': pretty_timedelta(changeset.date), 
    244226                'size': pretty_size(node.content_length), 
    245227                'author': changeset.author or 'anonymous', 
    246228                'message': message 
    247229            }  
    248  
     230             
    249231            # add ''Plain Text'' alternate link if needed 
    250             if not is_binary(chunk) and mime_type != 'text/plain': 
     232            if not content.is_binary and not TEXT_PLAIN.match(content.type): 
    251233                plain_href = req.href.browser(node.path, rev=rev, format='txt') 
    252                 add_link(req, 'alternate', plain_href, 'Plain Text', 
    253                          'text/plain') 
     234                add_link(req, 'alternate', plain_href,  
     235                         TEXT_PLAIN.name, TEXT_PLAIN.mimetype) 
    254236 
    255237            # 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) 
     238            add_link(req, 'alternate', content.url, 
     239                     'Original Format', content.type.mimetype) 
    258240 
    259             self.log.debug("Rendering preview of node %s@%s with mime-type %s" 
    260                            % (node.name, str(rev), mime_type)) 
     241            req.hdf['file'] = Mimeview(self.env).preview_to_hdf( 
     242                req, content, annotations=['lineno']) 
    261243 
    262             del content # the remainder of that content is not needed 
    263  
    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  
    268244            add_stylesheet(req, 'common/css/code.css') 
    269245 
    270246    # IWikiSyntaxProvider methods 
  • trac/wiki/web_ui.py

     
    2222 
    2323from trac.attachment import attachments_to_hdf, Attachment, AttachmentModule 
    2424from trac.core import * 
     25from trac.mimeview.api import * 
    2526from trac.perm import IPermissionRequestor 
    2627from trac.Search import ISearchSource, search_to_sql, shorten_result 
    2728from trac.Timeline import ITimelineEventProvider 
     
    3435from trac.web import HTTPNotFound, IRequestHandler 
    3536from trac.wiki.api import IWikiPageManipulator, WikiSystem 
    3637from 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 
     38from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner, \ 
     39                                TEXT_X_TRAC_WIKI, APPLICATION_X_TRAC_WIKI 
    3940 
    4041 
    4142class InvalidWikiPage(TracError): 
     
    5051    page_manipulators = ExtensionPoint(IWikiPageManipulator) 
    5152 
    5253    # IContentConverter methods 
    53     def get_supported_conversions(self): 
    54         yield ('txt', 'Plain Text', 'txt', 'text/x-trac-wiki', 'text/plain', 9) 
    5554 
    56     def convert_content(self, req, mimetype, content, key): 
    57         return (content, 'text/plain;charset=utf-8') 
     55    def get_supported_conversions(self, input): 
     56        if TEXT_X_TRAC_WIKI.match(input) or \ 
     57               APPLICATION_X_TRAC_WIKI.match(input): 
     58            yield Conversion(format='txt', quality=8, output=TEXT_PLAIN) 
     59            yield Conversion(format='html', quality=8, output=TEXT_HTML) 
    5860 
     61    def convert_content(self, context, conversion, content): 
     62        if conversion.format == 'txt': 
     63            return content # identity transform 
     64        elif conversion.format == 'html': 
     65            # TODO 0.11: give the context to `wiki_to_html` 
     66            html = wiki_to_html(unicode(content), self.env, context.req) 
     67            return StringMimeContent(self.env, html, TEXT_HTML) 
     68 
    5969    # INavigationContributor methods 
    6070 
    6171    def get_active_navigation_item(self, req): 
     
    129139        else: 
    130140            format = req.args.get('format') 
    131141            if format: 
    132                 Mimeview(self.env).send_converted(req, 'text/x-trac-wiki', 
    133                                                   page.text, format, page.name) 
     142                content = StringMimeContent(self.env, page.text, 
     143                                            TEXT_X_TRAC_WIKI, page.name) 
     144                content.convert(req, format).send(req) 
    134145            self._render_view(req, db, page) 
    135146 
    136147        req.hdf['wiki.action'] = action 
     
    430441            req.hdf['html.norobots'] = 1 
    431442 
    432443        # Add registered converters 
    433         for conversion in Mimeview(self.env).get_supported_conversions( 
    434                                              'text/x-trac-wiki'): 
    435             conversion_href = req.href.wiki(page.name, version=version, 
    436                                             format=conversion[0]) 
    437             add_link(req, 'alternate', conversion_href, conversion[1], 
    438                      conversion[3]) 
     444        for c in Mimeview(self.env).get_conversions(TEXT_X_TRAC_WIKI): 
     445            if c.format in ('default', 'html'): 
     446                continue 
     447            add_link(req, 'alternate', 
     448                     req.href.wiki(page.name, version=version, format=c.format), 
     449                     c.output.name, c.output.mimetype_charset) 
    439450 
    440451        latest_page = WikiPage(self.env, page.name) 
    441452        req.hdf['wiki'] = {'exists': page.exists, 
  • trac/wiki/formatter.py

     
    3131from trac.util.text import shorten_line, to_unicode 
    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, 
     
    5968            # Find a matching wiki macro 
    6069            for macro_provider in WikiSystem(self.env).macro_providers: 
    6170                for macro_name in macro_provider.get_macros(): 
    62                     if self.name == macro_name: 
     71                    if name == macro_name: 
    6372                        self.processor = self._macro_processor 
    6473                        self.macro_provider = macro_provider 
    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).get_mimetype(name) 
     78            if mimetype and not mimetype.is_binary: 
     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        content = StringMimeContent(self.env, text, self.mimetype) 
     112        return Mimeview(self.env).render(self.formatter, content) 
    105113 
    106114    def process(self, req, text, in_paragraph=False): 
    107115        if self.error: 
     
    157165    ENDBLOCK_TOKEN = r"\}\}\}" 
    158166    ENDBLOCK = "}}}" 
    159167     
    160     LINK_SCHEME = r"[\w.+-]+" # as per RFC 2396 
     168    LINK_SCHEME = r"[a-zA-Z][\w.+-]*" # RFC 2396, except upper case is also ok 
     169 
    161170    INTERTRAC_SCHEME = r"[a-zA-Z.+-]*?" # no digits (support for shorthand links) 
    162171 
    163172    QUOTED_STRING = r"'[^']+'|\"[^\"]+\""