Index: trac/mimeview/api.py =================================================================== --- trac/mimeview/api.py (revision 2737) +++ trac/mimeview/api.py (working copy) @@ -188,6 +188,162 @@ annotation data.""" +class MimeType: + """ Represents a MIME type. The first element of types is the primary MIME + type. """ + def __init__(self, description, types, extensions): + self.types, self.extensions, self.description = types, extensions, description + self.type = self.types[0] + +class IMimeTypeRegistry(Interface): + def lookup_mime_type(type): + """ Return MimeType object or None if MIME type not found. """ + + def lookup_mime_extension(extension): + """ Return MimeType object or None if file extension not found. """ + + def lookup_mime_content(content): + """ Try and determine the MIME type of an object by content. """ + +class IMimeConverter(Interface): + def get_mime_conversions(): + """ Return an iterator of (input_type, output_type, output_type_description) tuples. """ + + def perform_mime_conversion(req, source, input_type, output_type): + """ Perform conversion of a source "string" to an alternate MIME type. + Returns (content_type, output). """ + +class MimeSystem(Component): + """ Manage MIME types. """ + + implements(IMimeTypeRegistry) + + registries = ExtensionPoint(IMimeTypeRegistry) + converters = ExtensionPoint(IMimeConverter) + + _mime_registry_hardcoded = ( + MimeType('Plain Text', ('text/plain',), ('txt',)), + MimeType('HTML', ('text/html',), ('html', 'htm')), + MimeType('Trac Wiki Text', ('text/x-trac-wiki', 'application/x-trac-wiki'), ()) + ) + + def __init__(self): + self._mime_registry_types = {} + self._mime_registry_extensions = {} + + for mimetype in self._mime_registry_hardcoded: + for type in mimetype.types: + self._mime_registry_types[type] = mimetype + for ext in mimetype.extensions: + self._mime_registry_extensions[ext] = mimetype + + def lookup_extension(self, extension): + """ Look up the MIME type for a given extension. """ + for register in self.registries: + type = register.lookup_mime_extension(extension) + if type: + return type + raise TracError("No MIME type for extension %s" % extension) + + def lookup_type(self, type): + """ Look up MIME type details for a given type. """ + for register in self.registries: + mimetype = register.lookup_mime_type(type) + if mimetype: + return mimetype + raise TracError("No matching MIME type for %s" % type) + + def lookup_content(self, content): + """ Determine MIME type by content alone. """ + for register in self.registries: + mimetype = register.lookup_content(content) + if mimetype: + return mimetype + raise TracError("No matching MIME type for content") + + def lookup_filename(self, filename, content = None): + """ Try and determine MIME type details for a filename. """ + extension = filename.split('.')[-1] + try: + return self.lookup_extension(extension) + except TracError: + try: + if content: self.lookup_content(content) + except TracError: + raise TracError("No matching MIME type for filename or content") + + def lookup_conversions_from(self, input_type): + """ Return an iterable of types that input_type can be converted to. """ + for converter in self.converters: + for i, o, d in converter.get_mime_conversions(): + if i == input_type: + yield self.lookup_type(o) + + def lookup_conversions_to(self, output_type): + """ Return an iterable of types that output_type can be converted from. """ + for converter in self.converters: + for i, o, desc in converter.get_mime_conversions(): + if o == output_type: + yield self.lookup_type(i) + + def convert(self, req, source, input_type, output_type): + """ Convert source between MIME types. Returns (content_type, output). """ + for converter in self.converters: + for conversion in converter.get_mime_conversions(): + if conversion[0:2] == (input_type, output_type): + return converter.perform_mime_conversion(req, source, input_type, output_type) + raise TracError("No MIME conversion from %s to %s" % (input_type, output_type)) + + # IMimeTypeRegistry methods + + def _description_from_type(self, type): + description = type + # Fall back on the description from the MIME converters + for converter in self.converters: + for i, o, d in converter.get_mime_conversions(): + if o == type: + description = d + break + if description != type: + break + return description + + def lookup_mime_type(self, type): + try: + return self._mime_registry_types[type] + except: + description = self._description_from_type(type) + try: + import mimetypes + exts = [x[1:] for x in mimetypes.guess_all_extensions(type)] + return MimeType(description, (type,), exts) + except: + return None + + def lookup_mime_extension(self, extension): + try: + return self._mime_registry_extensions[extension] + except: + try: + import mimetypes + type = mimetypes.guess_type('foo.' + extension)[0] + description = self._description_from_type(type) + return MimeType(description, (type,), (extension,)) + except: + return None + + def lookup_mime_content(self, content): + global MODE_RE, MIME_MAP + match = re.search(MODE_RE, content[:1000]) + if match: + mode = match.group(1) or match.group(2).lower() + if MIME_MAP.has_key(mode): + return MIME_MAP[mode] + if content and is_binary(content): + return 'application/octet-stream' + return None + + class Mimeview(Component): """A generic class to prettify data, typically source code.""" Index: trac/wiki/web_ui.py =================================================================== --- trac/wiki/web_ui.py (revision 2737) +++ trac/wiki/web_ui.py (working copy) @@ -31,12 +31,13 @@ from trac.web import IRequestHandler from trac.wiki.model import WikiPage from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner +from trac.mimeview.api import MimeSystem, IMimeConverter class WikiModule(Component): implements(INavigationContributor, IPermissionRequestor, IRequestHandler, - ITimelineEventProvider, ISearchSource) + ITimelineEventProvider, ISearchSource, IMimeConverter) # INavigationContributor methods @@ -109,11 +110,17 @@ elif action == 'history': self._render_history(req, db, page) else: - if req.args.get('format') == 'txt': + if req.args.has_key('format'): + format = req.args.get('format') + # Backwards compatibility + if format == 'txt': + format = 'text/plain' + mimesystem = MimeSystem(self.env) + content_type, output = mimesystem.convert(req, page.text, 'text/x-trac-wiki', format) req.send_response(200) - req.send_header('Content-Type', 'text/plain;charset=utf-8') + req.send_header('Content-Type', content_type) req.end_headers() - req.write(page.text) + req.write(output) return self._render_view(req, db, page) @@ -357,8 +364,10 @@ # Ask web spiders to not index old versions req.hdf['html.norobots'] = 1 - txt_href = self.env.href.wiki(page.name, version=version, format='txt') - add_link(req, 'alternate', txt_href, 'Plain Text', 'text/plain') + mimesystem = MimeSystem(self.env) + for mimetype in mimesystem.lookup_conversions_from('text/x-trac-wiki'): + mime_href = self.env.href.wiki(page.name, version=version, format=mimetype.type) + add_link(req, 'alternate', mime_href, mimetype.description, mimetype.type) req.hdf['wiki'] = {'page_name': page.name, 'exists': page.exists, 'version': page.version, 'readonly': page.readonly} @@ -405,3 +414,15 @@ '%s: %s' % (name, shorten_line(text)), date, author, shorten_result(text, query.split())) + + # IMimeConverter methods + + def get_mime_conversions(self): + yield ('text/x-trac-wiki', 'text/plain', 'Plain text') + yield ('text/x-trac-wiki', 'text/html', 'HTML') + + def perform_mime_conversion(self, req, source, input_type, output_type): + if (input_type, output_type) == ('text/x-trac-wiki', 'text/plain'): + return ('text/plain;charset=utf-8', source) + else: + return ('text/html', wiki_to_html(source, self.env, req))