Edgewall Software

TracDev/ContextRefactoring: trac_mimeview_api.py

File trac_mimeview_api.py, 35.7 KB (added by cboos, 14 months ago)

Work in progress snapshot - the modifications to source:trunk/trac/mimeview/api.py are rather minimal, only the addition of the RenderingContext class, used to provide a stack of the resources associated to each rendered content (the top of the stack being the current context)

Line 
1# -*- coding: utf-8 -*-
2#
3# Copyright (C) 2004-2007 Edgewall Software
4# Copyright (C) 2004 Daniel Lundin <daniel@edgewall.com>
5# Copyright (C) 2005-2006 Christopher Lenz <cmlenz@gmx.de>
6# Copyright (C) 2006-2007 Christian Boos <cboos@neuf.fr>
7# All rights reserved.
8#
9# This software is licensed as described in the file COPYING, which
10# you should have received as part of this distribution. The terms
11# are also available at http://trac.edgewall.org/wiki/TracLicense.
12#
13# This software consists of voluntary contributions made by many
14# individuals. For the exact contribution history, see the revision
15# history and logs, available at http://trac.edgewall.org/log/.
16#
17# Author: Daniel Lundin <daniel@edgewall.com>
18#         Christopher Lenz <cmlenz@gmx.de>
19#         Christian Boos <cboos@neuf.fr>
20
21"""
22----
23NOTE: for plugin developers
24
25 The Mimeview API is quite complex and many things there are currently
26 a bit difficult to work with (e.g. what an actual `content` might be,
27 see last paragraph of this docstring).
28
29 So this area is mainly in a ''work in progress'' state, which will
30 be improved upon in the near future
31 (see http://trac.edgewall.org/ticket/3332).
32
33 In particular, if you are interested in writing IContentConverter
34 and IHTMLPreviewRenderer components, note that those interfaces
35 will be merged into a new style IContentConverter.
36 Feel free to contribute remarks and suggestions for improvements
37 to the corresponding ticket (#3332).
38----
39
40The `trac.mimeview` module centralize the intelligence related to
41file metadata, principally concerning the `type` (MIME type) of the content
42and, if relevant, concerning the text encoding (charset) used by the content.
43
44There are primarily two approaches for getting the MIME type of a given file:
45 * taking advantage of existing conventions for the file name
46 * examining the file content and applying various heuristics
47
48The module also knows how to convert the file content from one type
49to another type.
50
51In some cases, only the `url` pointing to the file's content is actually
52needed, that's why we avoid to read the file's content when it's not needed.
53
54The actual `content` to be converted might be a `unicode` object,
55but it can also be the raw byte string (`str`) object, or simply
56an object that can be `read()`.
57"""
58
59import re
60from StringIO import StringIO
61
62from genshi import Markup, Stream
63from genshi.core import TEXT, START, END, START_NS, END_NS
64from genshi.builder import Fragment, tag
65from genshi.input import HTMLParser
66
67from trac.config import IntOption, ListOption, Option
68from trac.core import *
69from trac.util import reversed, sorted, Ranges
70from trac.util.text import to_utf8, to_unicode
71
72
73__all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 'Mimeview',
74           'content_to_unicode', 'RenderingContext']
75
76
77# Some common MIME types and their associated keywords and/or file extensions
78
79KNOWN_MIME_TYPES = {
80    'application/pdf':        ['pdf'],
81    'application/postscript': ['ps'],
82    'application/rtf':        ['rtf'],
83    'application/x-sh':       ['sh'],
84    'application/x-csh':      ['csh'],
85    'application/x-troff':    ['nroff', 'roff', 'troff'],
86    'application/x-yaml':     ['yml', 'yaml'],
87   
88    'application/rss+xml':    ['rss'],
89    'application/xsl+xml':    ['xsl'],
90    'application/xslt+xml':   ['xslt'],
91   
92    'image/x-icon':           ['ico'],
93    'image/svg+xml':          ['svg'],
94   
95    'model/vrml':             ['vrml', 'wrl'],
96   
97    'text/css':               ['css'],
98    'text/html':              ['html'],
99    'text/plain':             ['txt', 'TXT', 'text', 'README', 'INSTALL',
100                               'AUTHORS', 'COPYING', 'ChangeLog', 'RELEASE'],
101    'text/xml':               ['xml'],
102    'text/x-csrc':            ['c', 'xs'],
103    'text/x-chdr':            ['h'],
104    'text/x-c++src':          ['cc', 'CC', 'cpp', 'C'],
105    'text/x-c++hdr':          ['hh', 'HH', 'hpp', 'H'],
106    'text/x-csharp':          ['cs'],
107    'text/x-diff':            ['diff', 'patch'],
108    'text/x-eiffel':          ['e'],
109    'text/x-elisp':           ['el'],
110    'text/x-fortran':         ['f'],
111    'text/x-haskell':         ['hs'],
112    'text/x-javascript':      ['js'],
113    'text/x-objc':            ['m', 'mm'],
114    'text/x-ocaml':           ['ml', 'mli'],
115    'text/x-makefile':        ['make', 'mk',
116                               'Makefile', 'makefile', 'GNUMakefile'],
117    'text/x-pascal':          ['pas'],
118    'text/x-perl':            ['pl', 'pm', 'PL', 'perl'],
119    'text/x-php':             ['php', 'php3', 'php4'],
120    'text/x-python':          ['py', 'python'],
121    'text/x-pyrex':           ['pyx'],
122    'text/x-ruby':            ['rb', 'ruby'],
123    'text/x-scheme':          ['scm'],
124    'text/x-textile':         ['txtl', 'textile'],
125    'text/x-vba':             ['vb', 'vba', 'bas'],
126    'text/x-verilog':         ['v', 'verilog'],
127    'text/x-vhdl':            ['vhd'],
128}
129
130# extend the above with simple (text/x-<something>: <something>) mappings
131
132for x in ['ada', 'asm', 'asp', 'awk', 'idl', 'inf', 'java', 'ksh', 'lua',
133          'm4', 'mail', 'psp', 'rfc', 'rst', 'sql', 'tcl', 'tex', 'zsh']:
134    KNOWN_MIME_TYPES.setdefault('text/x-%s' % x, []).append(x)
135
136
137# Default mapping from keywords/extensions to known MIME types:
138
139MIME_MAP = {}
140for t, exts in KNOWN_MIME_TYPES.items():
141    MIME_MAP[t] = t
142    for e in exts:
143        MIME_MAP[e] = t
144
145# Simple builtin autodetection from the content using a regexp
146MODE_RE = re.compile(
147    r"#!.+?env (\w+)|"                       # look for shebang with env
148    r"#!(?:[/\w.-_]+/)?(\w+)|"               # look for regular shebang
149    r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*-
150    r"vim:.*?(?:syntax|filetype|ft)=(\w+)"   # look for VIM's syntax=<n>
151    )
152
153def get_mimetype(filename, content=None, mime_map=MIME_MAP):
154    """Guess the most probable MIME type of a file with the given name.
155
156    `filename` is either a filename (the lookup will then use the suffix)
157    or some arbitrary keyword.
158   
159    `content` is either a `str` or an `unicode` string.
160    """
161    suffix = filename.split('.')[-1]
162    if suffix in mime_map:
163        # 1) mimetype from the suffix, using the `mime_map`
164        return mime_map[suffix]
165    else:
166        mimetype = None
167        try:
168            import mimetypes
169            # 2) mimetype from the suffix, using the `mimetypes` module
170            mimetype = mimetypes.guess_type(filename)[0]
171        except:
172            pass
173        if not mimetype and content:
174            match = re.search(MODE_RE, content[:1000] + content[-1000:])
175            if match:
176                mode = match.group(1) or match.group(2) or match.group(4) or \
177                    match.group(3).lower()
178                if mode in mime_map:
179                    # 3) mimetype from the content, using the `MODE_RE`
180                    return mime_map[mode]
181            else:
182                if is_binary(content):
183                    # 4) mimetype from the content, using`is_binary`
184                    return 'application/octet-stream'
185        return mimetype
186
187def is_binary(data):
188    """Detect binary content by checking the first thousand bytes for zeroes.
189
190    Operate on either `str` or `unicode` strings.
191    """
192    if isinstance(data, str) and detect_unicode(data):
193        return False
194    return '\0' in data[:1000]
195
196def detect_unicode(data):
197    """Detect different unicode charsets by looking for BOMs (Byte Order Marks).
198
199    Operate obviously only on `str` objects.
200    """
201    if data.startswith('\xff\xfe'):
202        return 'utf-16-le'
203    elif data.startswith('\xfe\xff'):
204        return 'utf-16-be'
205    elif data.startswith('\xef\xbb\xbf'):
206        return 'utf-8'
207    else:
208        return None
209
210def content_to_unicode(env, content, mimetype):
211    """Retrieve an `unicode` object from a `content` to be previewed"""
212    mimeview = Mimeview(env)
213    if hasattr(content, 'read'):
214        content = content.read(mimeview.max_preview_size)
215    return mimeview.to_unicode(content, mimetype)
216
217
218class RenderingContext(object):
219    """Rendering contexts.
220
221    This specifies ''how'' a rendering should be done, with various
222    options that might be relevant to some or all the renderers.
223
224    A context stores the `href` used as a base for building URLs.
225    If not given, this reverts to the value of `env.abs_href`.
226   
227    A resource can also be associated to a rendering context, and that resource
228    will be used as the base content when rendering relative TracLinks.
229
230    Another aspect related to the access context consists of the scope or
231    context trail by which the information belonging to a context is
232    presented. It is quite usual that contexts are embedded in other
233    contexts. This can be known by querying the `parent` context, which
234    is automatically set when creating a subcontext from another context. XXX
235
236    For example, when rendering a ticket description from within a
237    Custom Query rendered by the TicketQuery macro inside a wiki page,
238    the context ''path'' will be:
239
240    context.resource.(realm, id) = ('ticket', '12')
241    context.parent.resource.(realm, id) = ('wiki', 'CurrentStatus')
242
243    Finally, the context could also know about the expected output MIME type
244    which should be used to present the information to the user (TODO)
245    """
246
247    def __init__(self, req, resource, parent=None, href=None, **kwargs):
248        self.req = req
249        self.resource = resource or req.perm.toplevel()
250        self.env = self.resource.env
251        self.perm = self.resource.perm
252        self.parent = parent
253        self.href = href or self.env.abs_href
254        self.properties = kwargs
255
256    def __repr__(self):
257        path = []
258        context = self
259        while context:
260            if context.resource.realm: # skip toplevel resource
261                path.append(unicode(context.resource))
262            context = context.parent
263        return '<RenderingContext %s>' % (' - '.join(reversed(path)))
264
265    def __call__(self, resource, href=None, **kwargs):
266        """Return a sub-`RenderingContext`.
267
268        'href' can be modified on the fly, which is usefull to e.g. turn URL
269        generation to absolute mode.
270
271        'resource' can be modified this way as well.
272        Any remaining keyword argument will be treated as a new property.
273        """
274        return RenderingContext(self.req, resource, self, href or self.href,
275                                **kwargs)
276   
277    def __contains__(self, resource):
278        """Check whether a given resource is in the rendering path.
279
280        This is useful for avoiding to render resources recursively.
281        """
282        context = self
283        while context:
284            if context.resource == resource:
285                return True
286            context = context.parent
287
288
289class IHTMLPreviewRenderer(Interface):
290    """Extension point interface for components that add HTML renderers of
291    specific content types to the `Mimeview` component.
292
293    ----
294    This interface will be merged with IContentConverter, as conversion
295    to text/html will be simply a particular type of content conversion.
296
297    However, note that the IHTMLPreviewRenderer will still be supported
298    for a while through an adapter, whereas the IContentConverter interface
299    itself will be changed.
300
301    So if all you want to do is convert to HTML and don't feel like
302    following the API changes, rather you should rather implement this
303    interface for the time being.
304    ---
305    """
306
307    # implementing classes should set this property to True if they
308    # support text content where Trac should expand tabs into spaces
309    expand_tabs = False
310
311    # indicate whether the output of this renderer is source code that can
312    # be decorated with annotations
313    returns_source = False
314
315    def get_quality_ratio(mimetype):
316        """Return the level of support this renderer provides for the `content`
317        of the specified MIME type. The return value must be a number between
318        0 and 9, where 0 means no support and 9 means "perfect" support.
319        """
320
321    def render(context, mimetype, content, filename=None, url=None):
322        """Render an XHTML preview of the raw `content` in context.
323
324        `context` is a `RenderingContext`.
325
326        The `content` might be:
327         * a `str` object
328         * an `unicode` string
329         * any object with a `read` method, returning one of the above
330
331        It is assumed that the content will correspond to the given `mimetype`.
332
333        Besides the `content` value, the same content may eventually
334        be available through the `filename` or `url` parameters.
335        This is useful for renderers that embed objects, using <object> or
336        <img> instead of including the content inline.
337       
338        Can return the generated XHTML text as a single string or as an
339        iterable that yields strings. In the latter case, the list will
340        be considered to correspond to lines of text in the original content.
341        """
342
343
344class IHTMLPreviewAnnotator(Interface):
345    """Extension point interface for components that can annotate an XHTML
346    representation of file contents with additional information."""
347
348    def get_annotation_type():
349        """Return a (type, label, description) tuple
350        that defines the type of annotation and provides human readable names.
351        The `type` element should be unique to the annotator.
352        The `label` element is used as column heading for the table,
353        while `description` is used as a display name to let the user
354        toggle the appearance of the annotation type.
355        """
356       
357    def get_annotation_data(context):
358        """Return some metadata to be used by the `annotate_row` method below.
359
360        This will be called only once, before lines are processed.
361        If this raises an error, that annotator won't be used.
362        """
363
364    def annotate_row(context, row, number, line, data):
365        """Return the XHTML markup for the table cell that contains the
366        annotation data.
367
368        `context` is the `RenderingContext` corresponding to the content being
369        annotated,
370        `row` is the tr Element being built, `number` is the line number being
371        processed and `line` is the line's actual content.
372        `annotations` is whatever additional data the `get_annotation_data`
373        method decided to provide.
374        """
375
376
377class IContentConverter(Interface):
378    """An extension point interface for generic MIME based content
379    conversion.
380
381    ----
382    NOTE: This api will likely change in the future, e.g.:
383
384    def get_supported_conversions(input):
385        '''Tells whether this converter can handle this `input` type.
386
387        Return an iterable of `Conversion` objects, each describing
388        how the conversion should be done and what will be the output type.
389        '''
390
391    def convert_content(context, conversion, content):
392        '''Convert the given `AbstractContent` as specified by `Conversion`.
393
394        The conversion takes place in the given formatting `context`.
395        A `context` provides at least a `req` property.
396        If no other specific context object is available, a
397        `ToplevelContext` can be used to wrap the `req` instance.
398       
399        Return the converted content, which ''must'' be a `MimeContent` object.
400        '''
401    ----
402    """
403
404    def get_supported_conversions():
405        """Return an iterable of tuples in the form (key, name, extension,
406        in_mimetype, out_mimetype, quality) representing the MIME conversions
407        supported and
408        the quality ratio of the conversion in the range 0 to 9, where 0 means
409        no support and 9 means "perfect" support. eg. ('latex', 'LaTeX', 'tex',
410        'text/x-trac-wiki', 'text/plain', 8)"""
411
412    def convert_content(req, mimetype, content, key):
413        """Convert the given content from mimetype to the output MIME type
414        represented by key. Returns a tuple in the form (content,
415        output_mime_type) or None if conversion is not possible."""
416
417
418class Mimeview(Component):
419    """A generic class to prettify data, typically source code."""
420
421    renderers = ExtensionPoint(IHTMLPreviewRenderer)
422    annotators = ExtensionPoint(IHTMLPreviewAnnotator)
423    converters = ExtensionPoint(IContentConverter)
424
425    default_charset = Option('trac', 'default_charset', 'iso-8859-15',
426        """Charset to be used when in doubt.""")
427
428    tab_width = IntOption('mimeviewer', 'tab_width', 8,
429        """Displayed tab width in file preview (''since 0.9'').""")
430
431    max_preview_size = IntOption('mimeviewer', 'max_preview_size', 262144,
432        """Maximum file size for HTML preview. (''since 0.9'').""")
433
434    mime_map = ListOption('mimeviewer', 'mime_map',
435        'text/x-dylan:dylan,text/x-idl:ice,text/x-ada:ads:adb',
436        doc="""List of additional MIME types and keyword mappings.
437        Mappings are comma-separated, and for each MIME type,
438        there's a colon (":") separated list of associated keywords
439        or file extensions. (''since 0.10'').""")
440
441    def __init__(self):
442        self._mime_map = None
443
444    # Public API
445
446    def get_supported_conversions(self, mimetype):
447        """Return a list of target MIME types in same form as
448        `IContentConverter.get_supported_conversions()`, but with the converter
449        component appended. Output is ordered from best to worst quality."""
450        converters = []
451        for converter in self.converters:
452            for k, n, e, im, om, q in converter.get_supported_conversions():
453                if im == mimetype and q > 0:
454                    converters.append((k, n, e, im, om, q, converter))
455        converters = sorted(converters, key=lambda i: i[-2], reverse=True)
456        return converters
457
458    def convert_content(self, req, mimetype, content, key, filename=None,
459                        url=None):
460        """Convert the given content to the target MIME type represented by
461        `key`, which can be either a MIME type or a key. Returns a tuple of
462        (content, output_mime_type, extension)."""
463        if not content:
464            return ('', 'text/plain;charset=utf-8', '.txt')
465
466        # Ensure we have a MIME type for this content
467        full_mimetype = mimetype
468        if not full_mimetype:
469            if hasattr(content, 'read'):
470                content = content.read(self.max_preview_size)
471            full_mimetype = self.get_mimetype(filename, content)
472        if full_mimetype:
473            mimetype = full_mimetype.split(';')[0].strip() # split off charset
474        else:
475            mimetype = full_mimetype = 'text/plain' # fallback if not binary
476
477        # Choose best converter
478        candidates = list(self.get_supported_conversions(mimetype))
479        candidates = [c for c in candidates if key in (c[0], c[4])]
480        if not candidates:
481            raise TracError('No available MIME conversions from %s to %s' %
482                            (mimetype, key))
483
484        # First successful conversion wins
485        for ck, name, ext, input_mimettype, output_mimetype, quality, \
486                converter in candidates:
487            output = converter.convert_content(req, mimetype, content, ck)
488            if not output:
489                continue
490            return (output[0], output[1], ext)
491        raise TracError('No available MIME conversions from %s to %s' %
492                        (mimetype, key))
493
494    def get_annotation_types(self):
495        """Generator that returns all available annotation types."""
496        for annotator in self.annotators:
497            yield annotator.get_annotation_type()
498
499    def render(self, context, mimetype, content, filename=None, url=None,
500               annotations=None, force_source=False):
501        """Render an XHTML preview of the given `content`.
502
503        `content` is the same as an `IHTMLPreviewRenderer.render`'s
504        `content` argument.
505
506        The specified `mimetype` will be used to select the most appropriate
507        `IHTMLPreviewRenderer` implementation available for this MIME type.
508        If not given, the MIME type will be infered from the filename or the
509        content.
510
511        Return a string containing the XHTML text.
512        """
513        if not content:
514            return ''
515
516        # Ensure we have a MIME type for this content
517        full_mimetype = mimetype
518        if not full_mimetype:
519            if hasattr(content, 'read'):
520                content = content.read(self.max_preview_size)
521            full_mimetype = self.get_mimetype(filename, content)
522        if full_mimetype:
523            mimetype = full_mimetype.split(';')[0].strip() # split off charset
524        else:
525            mimetype = full_mimetype = 'text/plain' # fallback if not binary
526
527        # Determine candidate `IHTMLPreviewRenderer`s
528        candidates = []
529        for renderer in self.renderers:
530            qr = renderer.get_quality_ratio(mimetype)
531            if qr > 0:
532                candidates.append((qr, renderer))
533        candidates.sort(lambda x,y: cmp(y[0], x[0]))
534
535        # First candidate which renders successfully wins.
536        # Also, we don't want to expand tabs more than once.
537        expanded_content = None
538        errors = []
539        for qr, renderer in candidates:
540            if force_source and not getattr(renderer, 'returns_source', False):
541                continue # skip non-source renderers in force_source mode
542            try:
543                ann_names = annotations and ', '.join(annotations) or \
544                           'No annotations'
545                self.log.debug('Trying to render HTML preview using %s [%s]'
546                               % (renderer.__class__.__name__, ann_names))
547
548                # check if we need to perform a tab expansion
549                rendered_content = content
550                if getattr(renderer, 'expand_tabs', False):
551                    if expanded_content is None:
552                        content = content_to_unicode(self.env, content,
553                                                     full_mimetype)
554                        expanded_content = content.expandtabs(self.tab_width)
555                    rendered_content = expanded_content
556
557                result = renderer.render(context, full_mimetype,
558                                         rendered_content, filename, url)
559                if not result:
560                    continue
561
562                if not (force_source or getattr(renderer, 'returns_source',
563                                                False)):
564                    # Direct rendering of content
565                    if isinstance(result, basestring):
566                        if not isinstance(result, unicode):
567                            result = to_unicode(result)
568                        return Markup(to_unicode(result))
569                    elif isinstance(result, Fragment):
570                        return result.generate()
571                    else:
572                        return result
573
574                # Render content as source code
575                if annotations:
576                    m = context.req and context.req.args.get('marks') or None
577                    return self._render_source(context, result, annotations,
578                                               m and Ranges(m))
579                else:
580                    if isinstance(result, list):
581                        result = Markup('\n').join(result)
582                    return tag.div(class_='code')(tag.pre(result)).generate()
583
584            except Exception, e:
585                self.log.warning('HTML preview using %s failed (%s)' %
586                                 (renderer, e), exc_info=True)
587                errors.append((renderer, e))
588        return errors
589
590    def _render_source(self, context, stream, annotations, marks=None):
591        annotators, labels, titles = {}, {}, {}
592        for annotator in self.annotators:
593            atype, alabel, atitle = annotator.get_annotation_type()
594            if atype in annotations:
595                labels[atype] = alabel
596                titles[atype] = atitle
597                annotators[atype] = annotator
598
599        if isinstance(stream, list):
600            stream = HTMLParser(StringIO('\n'.join(stream)))
601        elif isinstance(stream, unicode):
602            text = stream
603            def linesplitter():
604                for line in text.splitlines(True):
605                    yield TEXT, line, (None, -1, -1)
606            stream = linesplitter()
607
608        annotator_datas = []
609        for a in annotations:
610            annotator = annotators[a]
611            try:
612                data = (annotator, annotator.get_annotation_data(context))
613            except TracError, e:
614                self.log.warning("Can't use annotator '%s': %s" %
615                                 (a, e.message))
616                context.req.warning(tag.strong("Can't use ", tag.em(a),
617                                               " annotator:") +
618                                    tag.pre(e.message))
619                data = (None, None)
620            annotator_datas.append(data)
621
622        def _head_row():
623            return tag.tr(
624                [tag.th(labels[a], class_=a, title=titles[a])
625                 for a in annotations] +
626                [tag.th(u'\xa0', class_='content')]
627            )
628
629        def _body_rows():
630            for idx, line in enumerate(_group_lines(stream)):
631                row = tag.tr()
632                if marks and idx + 1 in marks:
633                    row(class_='hilite')
634                for annotator, data in annotator_datas:
635                    if annotator:
636                        annotator.annotate_row(context, row, idx+1, line, data)
637                    else:
638                        row.append(tag.td())
639                row.append(tag.td(line))
640                yield row
641
642        return tag.table(class_='code')(
643            tag.thead(_head_row()),
644            tag.tbody(_body_rows())
645        )
646
647    def get_max_preview_size(self):
648        """Deprecated: use `max_preview_size` attribute directly."""
649        return self.max_preview_size
650
651    def get_charset(self, content='', mimetype=None):
652        """Infer the character encoding from the `content` or the `mimetype`.
653
654        `content` is either a `str` or an `unicode` object.
655       
656        The charset will be determined using this order:
657         * from the charset information present in the `mimetype` argument
658         * auto-detection of the charset from the `content`
659         * the configured `default_charset`
660        """
661        if mimetype:
662            ctpos = mimetype.find('charset=')
663            if ctpos >= 0:
664                return mimetype[ctpos + 8:].strip()
665        if isinstance(content, str):
666            utf = detect_unicode(content)
667            if utf is not None:
668                return utf
669        return self.default_charset
670
671    def get_mimetype(self, filename, content=None):
672        """Infer the MIME type from the `filename` or the `content`.
673
674        `content` is either a `str` or an `unicode` object.
675
676        Return the detected MIME type, augmented by the
677        charset information (i.e. "<mimetype>; charset=..."),
678        or `None` if detection failed.
679        """
680        # Extend default extension to MIME type mappings with configured ones
681        if not self._mime_map:
682            self._mime_map = MIME_MAP
683            for mapping in self.config['mimeviewer'].getlist('mime_map'):
684                if ':' in mapping:
685                    assocations = mapping.split(':')
686                    for keyword in assocations: # Note: [0] kept on purpose
687                        self._mime_map[keyword] = assocations[0]
688
689        mimetype = get_mimetype(filename, content, self._mime_map)
690        charset = None
691        if mimetype:
692            charset = self.get_charset(content, mimetype)
693        if mimetype and charset and not 'charset' in mimetype:
694            mimetype += '; charset=' + charset
695        return mimetype
696
697    def to_utf8(self, content, mimetype=None):
698        """Convert an encoded `content` to utf-8.
699
700        ''Deprecated in 0.10. You should use `unicode` strings only.''
701        """
702        return to_utf8(content, self.get_charset(content, mimetype))
703
704    def to_unicode(self, content, mimetype=None, charset=None):
705        """Convert `content` (an encoded `str` object) to an `unicode` object.
706
707        This calls `trac.util.to_unicode` with the `charset` provided,
708        or the one obtained by `Mimeview.get_charset()`.
709        """
710        if not charset:
711            charset = self.get_charset(content, mimetype)
712        return to_unicode(content, charset)
713
714    def configured_modes_mapping(self, renderer):
715        """Return a MIME type to `(mode,quality)` mapping for given `option`"""
716        types, option = {}, '%s_modes' % renderer
717        for mapping in self.config['mimeviewer'].getlist(option):
718            if not mapping:
719                continue
720            try:
721                mimetype, mode, quality = mapping.split(':')
722                types[mimetype] = (mode, int(quality))
723            except (TypeError, ValueError):
724                self.log.warning("Invalid mapping '%s' specified in '%s' "
725                                 "option." % (mapping, option))
726        return types
727   
728    def preview_data(self, context, content