Ticket #3332: mimeview_api_refactoring-r3612.2.diff
| File mimeview_api_refactoring-r3612.2.diff, 119.1 KB (added by cboos, 5 years ago) |
|---|
-
trac/attachment.py
26 26 from trac.config import BoolOption, IntOption 27 27 from trac.core import * 28 28 from trac.env import IEnvironmentSetupParticipant 29 from trac.mimeview import *29 from trac.mimeview.api import Mimeview, MimeType, FileMimeContent, TEXT_PLAIN 30 30 from trac.util import get_reporter_id, create_unique_file 31 31 from trac.util.datefmt import format_datetime, pretty_timedelta 32 32 from trac.util.html import Markup, html … … 124 124 def parent_href(self, req): 125 125 return req.href(self.parent_type, self.parent_id) 126 126 127 def _get_title(self): 127 def _get_title(self): # TODO: should extend to other `parent_type`s 128 128 return '%s%s: %s' % (self.parent_type == 'ticket' and '#' or '', 129 129 self.parent_id, self.filename) 130 130 title = property(_get_title) … … 245 245 select = classmethod(select) 246 246 delete_all = classmethod(delete_all) 247 247 248 def open(self): 248 def open(self): # deprecate, use FileMimeContent if necessary 249 249 self.env.log.debug('Trying to open attachment at %s', self.path) 250 250 try: 251 251 fd = open(self.path, 'rb') … … 523 523 'author': get_reporter_id(req)} 524 524 525 525 def _render_view(self, req, attachment): 526 # FIXME: perm_map should extend to other `parent_type`s 526 527 perm_map = {'ticket': 'TICKET_VIEW', 'wiki': 'WIKI_VIEW'} 527 528 req.perm.assert_permission(perm_map[attachment.parent_type]) 528 529 … … 533 534 req.hdf['attachment'] = attachment_to_hdf(self.env, req, None, 534 535 attachment) 535 536 # 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) 538 539 539 540 perm_map = {'ticket': 'TICKET_ADMIN', 'wiki': 'WIKI_DELETE'} 540 541 if req.perm.has_permission(perm_map[attachment.parent_type]): 541 542 req.hdf['attachment.can_delete'] = 1 542 543 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') 546 547 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) 553 563 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) 569 571 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) 576 575 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']) 580 578 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 590 579 def _render_list(self, req, p_type, p_id): 591 580 self._parent_to_hdf(req, p_type, p_id) 592 581 req.hdf['attachment'] = { -
trac/mimeview/rst.py
28 28 import re 29 29 30 30 from trac.core import * 31 from trac.mimeview.api import IHTMLPreviewRenderer, content_to_unicode 31 from trac.mimeview.api import IContentConverter, MimeType, TEXT_HTML, \ 32 Conversion, StringMimeContent 32 33 from trac.util.html import Element 33 34 from trac.web.href import Href 34 35 from trac.wiki.formatter import WikiProcessor … … 38 39 """ 39 40 Renders plain text in reStructuredText format as HTML. 40 41 """ 41 implements(I HTMLPreviewRenderer)42 implements(IContentConverter) 42 43 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) 47 47 48 def render(self, req, mimetype, content, filename=None, rev=None): 48 def convert_content(self, context, conversion, content): 49 req = context.req 49 50 try: 50 51 from docutils import nodes 51 52 from docutils.core import publish_parts … … 180 181 181 182 _inliner = rst.states.Inliner() 182 183 _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, 185 186 settings_overrides={'halt_level': 6, 186 187 'file_insertion_enabled': 0, 187 188 '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
19 19 # Christian Boos <cboos@neuf.fr> 20 20 21 21 """ 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. 22 The `trac.mimeview` module centralizes the intelligence about typed content. 25 23 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 24 Originally, this was about file metadata, principally its MIME type and 25 eventually the text encoding (charset) used by that content. 29 26 30 The module also knows how to convert the file content from one type 31 to another type. 27 Now, this has evolved into managing any kind of typed content, 28 and deals also with converting a content from one type to another. 29 A common situation which is now handled part of the general case 30 is the conversion of any kind of content to a text/html representation. 32 31 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. 32 In order to keep the API of conversion interface IContentConverter simple, 33 we introduced a few classes, each encapsulating a part of the knowledge 34 related to the content and the conversion: 35 35 36 The actual `content` to be converted might be a `unicode` object, 37 but it can also be the raw byte string (`str`) object, or simply 38 an object that can be `read()`. 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. 39 75 """ 40 76 77 import os 41 78 import re 42 79 from StringIO import StringIO 43 80 44 81 from trac.config import IntOption, ListOption, Option 45 82 from trac.core import * 46 83 from trac.util import sorted 47 from trac.util.text import to_utf8, to_unicode 84 from trac.util.datefmt import http_date 85 from trac.util.text import to_utf8, to_unicode, IDENTITY_CHARSET 48 86 from trac.util.html import escape, Markup, Fragment, html 49 87 50 88 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'] 53 99 54 100 55 101 # Some common MIME types and their associated keywords and/or file extensions 56 102 103 APPLICATION_OCTET_STREAM_STR = 'application/octet-stream' 104 57 105 KNOWN_MIME_TYPES = { 58 106 'application/pdf': ['pdf'], 59 107 'application/postscript': ['ps'], … … 114 162 for e in exts: 115 163 MIME_MAP[e] = t 116 164 117 # Simple builtin autodetection from the content using a regexp118 MODE_RE = re.compile(119 r"#!(?:[/\w.-_]+/)?(\w+)|" # look for shebang120 r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*-121 r"vim:.*?syntax=(\w+)" # look for VIM's syntax=<n>122 )123 165 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 126 168 169 def get_mimetype_from_filename(filename, mime_map=MIME_MAP): 170 """Guess the most probable MIME type of file with the given `filename`. 171 127 172 `filename` is either a filename (the lookup will then use the suffix) 128 173 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. 131 177 """ 132 178 suffix = filename.split('.')[-1] 133 179 if suffix in mime_map: … … 141 187 mimetype = mimetypes.guess_type(filename)[0] 142 188 except: 143 189 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'156 190 return mimetype 157 191 192 # Simple builtin autodetection from the content using a regexp 193 MODE_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 202 def 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 225 def 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 158 233 def is_binary(data): 159 234 """Detect binary content by checking the first thousand bytes for zeroes. 160 235 … … 165 240 return '\0' in data[:1000] 166 241 167 242 def detect_unicode(data): 168 """Detect different unicode charsets by looking for B OMs (Byte Order Marks).243 """Detect different unicode charsets by looking for Byte Order Marks. 169 244 170 245 Operate obviously only on `str` objects. 171 246 """ … … 178 253 else: 179 254 return None 180 255 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)187 256 257 # -- ContentType and subclasses 188 258 189 class IHTMLPreviewRenderer(Interface): 190 """Extension point interface for components that add HTML renderers of 191 specific content types to the `Mimeview` component. 259 class ContentType(object): 260 """Abstract representation of the "type" of some content.""" 192 261 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 277 class 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. 194 282 """ 195 283 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__ 199 288 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 305 class 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. 204 331 """ 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 205 347 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 208 350 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 213 360 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") 215 370 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 402 TEXT_PLAIN = MimeType('text/plain', 'utf-8', 'Plain Text', 'txt') 403 TEXT_HTML = MimeType('text/html', 'utf-8', 'HTML', 'html') 404 TEXT_CSV = MimeType('text/csv', 'utf-8', 'Comma-delimited Text', 'csv') 405 TEXT_TSV = MimeType('text/tab-separated-values', 'utf-8', 406 'Tab-delimited Text', 'tsv') 407 408 APPLICATION_RSS_XML = MimeType('application/rss+xml', 'utf-8', 409 'RSS Feed', 'xml') 410 APPLICATION_OCTET_STREAM = MimeType(APPLICATION_OCTET_STREAM_STR, 411 IDENTITY_CHARSET, 412 'Undefined (binary)', 'bin') 413 414 415 # -- AbstractContent and subclasses 416 417 class 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: 220 453 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. 224 464 """ 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 225 471 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 510 class 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 531 class 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 775 class 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 807 class 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 825 class 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 839 class 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 893 class 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 899 class 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 931 class 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 226 956 class IHTMLPreviewAnnotator(Interface): 227 957 """Extension point interface for components that can annotate an XHTML 228 958 representation of file contents with additional information.""" 229 959 230 960 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. 236 968 """ 237 969 238 970 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. 241 972 973 The return value must be XHTML markup that fit in a table cell. 974 """ 242 975 243 class IContentConverter(Interface):244 """An extension point interface for generic MIME based content245 conversion."""246 976 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 254 978 255 def convert_content(req, mimetype, content, key):256 """Convert the given content from mimetype to the output MIME type257 represented by key. Returns a tuple in the form (content,258 output_mime_type) or None if conversion is not possible."""259 260 261 979 class Mimeview(Component): 262 980 """A generic class to prettify data, typically source code.""" 263 981 264 renderers = ExtensionPoint(IHTMLPreviewRenderer)265 982 annotators = ExtensionPoint(IHTMLPreviewAnnotator) 266 983 converters = ExtensionPoint(IContentConverter) 267 984 … … 275 992 """Maximum file size for HTML preview. (''since 0.9'').""") 276 993 277 994 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= 279 996 """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 keywords282 or file extensions. (''since 0.10'').""")283 997 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 284 1009 def __init__(self): 285 1010 self._mime_map = None 286 287 # Public API288 1011 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 300 1025 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) 308 1027 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. 319 1030 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. 326 1032 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) 336 1039 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`. 341 1044 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)) 345 1053 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) 348 1055 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. 353 1058 354 Return a string containing the XHTML text.1059 Otherwise, it's the same as `get_converters`. 355 1060 """ 356 if not content: 357 return '' 1061 return [conversion for conversion, _ in self.get_converters(input)] 358 1062 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) 369 1064 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`. 377 1067 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... 413 1079 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 414 1090 def _annotate(self, lines, annotations): 1091 """Add requested `annotations` to the lines' content.""" 415 1092 buf = StringIO() 416 1093 buf.write('<table class="code"><thead><tr>') 417 1094 annotators = [] … … 445 1122 buf.write('</tbody></table>') 446 1123 return buf.getvalue() 447 1124 1125 # -- Deprecated API (TODO: remove in 0.11) 1126 448 1127 def get_max_preview_size(self): 449 1128 """Deprecated: use `max_preview_size` attribute directly.""" 450 1129 return self.max_preview_size 451 1130 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` argument459 * 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 utf470 return self.default_charset471 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 the478 charset information (i.e. "<mimetype>; charset=..."),479 or `None` if detection failed.480 """481 # Extend default extension to MIME type mappings with configured ones482 if not self._mime_map:483 self._mime_map = MIME_MAP484 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 purpose488 self._mime_map[keyword] = assocations[0]489 490 mimetype = get_mimetype(filename, content, self._mime_map)491 charset = None492 if mimetype:493 charset = self.get_charset(content, mimetype)494 if mimetype and charset and not 'charset' in mimetype:495 mimetype += '; charset=' + charset496 return mimetype497 498 1131 def to_utf8(self, content, mimetype=None): 499 1132 """Convert an encoded `content` to utf-8. 500 1133 501 1134 ''Deprecated in 0.10. You should use `unicode` strings only.'' 502 1135 """ 503 return to_utf8(content , self.get_charset(content, mimetype))1136 return to_utf8(content) 504 1137 505 def to_unicode(self, content, mimetype=None, charset=None): 506 """Convert `content` (an encoded `str` object) to an `unicode` object. 1138 # -- Utilities 507 1139 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. 510 1148 """ 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`"""517 1149 types, option = {}, '%s_modes' % renderer 518 1150 for mapping in self.config['mimeviewer'].getlist(option): 519 1151 if not mapping: … … 526 1158 "option." % (mapping, option)) 527 1159 return types 528 1160 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`. 532 1163 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: 536 1169 return {'max_file_size_reached': True, 537 1170 'max_file_size': self.max_preview_size, 538 'raw_href': url}1171 'raw_href': content.url} 539 1172 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} 543 1180 544 def send_converted(self, req, in_type, content, selector, filename='file'):545 """Helper method for converting `content` and sending it directly.546 1181 547 `selector` can be either a key or a MIME Type."""548 from trac.web import RequestDone549 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 RequestDone558 559 1182 1183 # utility for Mimeview._annotate 560 1184 def _html_splitlines(lines): 561 1185 """Tracks open and close tags in lines of HTML text and yields lines that 562 1186 have no tags spanning more than one line.""" … … 604 1228 def annotate_line(self, number, content): 605 1229 return '<th id="L%s"><a href="#L%s">%s</a></th>' % (number, number, 606 1230 number) 1231 # return html.TH(html.A(number, href="#L%s" % number), id=number) 607 1232 608 1233 609 # -- Default renderers1234 # -- Default TEXT_HTML converters 610 1235 611 1236 class 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. 614 1241 """ 615 implements(I HTMLPreviewRenderer)1242 implements(IContentConverter) 616 1243 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) 618 1263 619 TREAT_AS_BINARY = [620 'application/pdf',621 'application/postscript',622 'application/rtf'623 ]624 1264 625 def get_quality_ratio(self, mimetype): 626 if mimetype in self.TREAT_AS_BINARY: 627 return 0 628 return 1 1265 class ImageRenderer(Component): 1266 """Inline image display. 629 1267 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 """ 634 1270 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) 639 1272 1273 IMAGE_TYPE = MimeType(re.compile(r'image/')) 640 1274 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) 644 1278 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) 649 1284 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")654 1285 1286 # ---- Backward compatibility support for IHTMLPreviewRenderer 1287 # 1288 # (TODO: remove in 0.11) 1289 # 655 1290 656 class WikiTextRenderer(Component):657 """ Render files containing Trac's own Wiki formatting markup."""658 implements(IHTMLPreviewRenderer)1291 class IHTMLPreviewRenderer(Interface): 1292 """Extension point interface for components that add HTML renderers of 1293 specific content types to the `Mimeview` component. 659 1294 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 """ 664 1297 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 1309 class 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
24 24 25 25 from trac.core import * 26 26 from trac.config import ListOption 27 from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 27 from trac.mimeview.api import IContentConverter, Mimeview, StringMimeContent, \ 28 Conversion, TEXT_HTML 28 29 29 30 __all__ = ['SilverCityRenderer'] 30 31 … … 57 58 CRLF_RE = re.compile('\r$', re.MULTILINE) 58 59 59 60 60 class SilverCity Renderer(Component):61 class SilverCityConverter(Component): 61 62 """Syntax highlighting based on SilverCity.""" 62 63 63 implements(I HTMLPreviewRenderer)64 implements(IContentConverter) 64 65 65 enscript_modes = ListOption('mimeviewer', 'silvercity_modes', 66 '', 66 silvercity_modes = ListOption('mimeviewer', 'silvercity_modes', doc= 67 67 """List of additional MIME types known by SilverCity. 68 68 69 For each, a tuple `mimetype:mode:quality` has to be 69 70 specified, where `mimetype` is the MIME type, 70 71 `mode` is the corresponding SilverCity mode to be used … … 79 80 def __init__(self): 80 81 self._types = None 81 82 82 def get_ quality_ratio(self, mimetype):83 def get_supported_conversions(self, input): 83 84 # Extend default MIME type to mode mappings with configured ones 84 85 if not self._types: 85 86 self._types = {} 86 87 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) 90 93 91 def render(self, req, mimetype, content, filename=None, rev=None):94 def convert_content(self, context, conversion, input): 92 95 import SilverCity 93 96 try: 94 mimetype = mimetype.split(';', 1)[0] 95 typelang = self._types[mimetype] 97 typelang = self._types[input.type.mimetype] 96 98 lang = typelang[0] 97 99 module = getattr(SilverCity, lang) 98 100 generator = getattr(module, lang + "HTMLGenerator") … … 108 110 raise Exception, err 109 111 110 112 # SilverCity does not like unicode strings 111 content = content.encode('utf-8')113 content = input.encode('utf-8') 112 114 113 115 # SilverCity generates extra empty line against some types of 114 116 # the line such as comment or #include with CRLF. So we … … 122 124 span_default_re = re.compile(r'<span class="\w+_default">(.*?)</span>', 123 125 re.DOTALL) 124 126 html = span_default_re.sub(r'\1', br_re.sub('', buf.getvalue())) 125 126 # Convert the output back to a unicode string127 html = html.decode('utf-8')128 127 129 128 # SilverCity generates _way_ too many non-breaking spaces... 130 129 # We don't need them anyway, so replace them by normal spaces 131 return html.replace(' ', ' ').splitlines() 130 return StringMimeContent(self.env, html.replace(' ', ' '), 131 TEXT_HTML) -
trac/mimeview/patch.py
16 16 # Ludvig Strigeus 17 17 18 18 from trac.core import * 19 from trac.mimeview.api import content_to_unicode,IHTMLPreviewRenderer, Mimeview19 from trac.mimeview.api import IHTMLPreviewRenderer, Mimeview 20 20 from trac.util.html import escape, Markup 21 21 from trac.web.chrome import add_stylesheet 22 22 … … 65 65 def render(self, req, mimetype, content, filename=None, rev=None): 66 66 from trac.web.clearsilver import HDFWrapper 67 67 68 content = content_to_unicode(self.env, content, mimetype)68 content = unicode(content) 69 69 d = self._diff_to_hdf(content.splitlines(), 70 70 Mimeview(self.env).tab_width) 71 71 if not d: -
trac/mimeview/enscript.py
135 135 cmdline += ' --color -h -q --language=html -p - -E%s' % mode 136 136 self.env.log.debug("Enscript command line: %s" % cmdline) 137 137 138 content = unicode(content) 138 139 np = NaivePopen(cmdline, content.encode('utf-8'), capturestderr=1) 139 140 if np.errorlevel or np.err: 140 141 err = 'Running (%s) failed: %s, %s.' % (cmdline, np.errorlevel, -
trac/mimeview/php.py
20 20 21 21 from trac.core import * 22 22 from trac.config import Option 23 from trac.mimeview.api import IHTMLPreviewRenderer , content_to_unicode23 from trac.mimeview.api import IHTMLPreviewRenderer 24 24 from trac.util import NaivePopen 25 25 from trac.util.html import Deuglifier 26 26 … … 79 79 cmdline += ' -sn' 80 80 self.env.log.debug("PHP command line: %s" % cmdline) 81 81 82 content = content_to_unicode(self.env, content, mimetype)82 content = unicode(content) 83 83 content = content.encode('utf-8') 84 84 np = NaivePopen(cmdline, content, capturestderr=1) 85 85 if np.errorlevel or np.err: -
trac/ticket/web_ui.py
23 23 from trac.config import BoolOption, Option 24 24 from trac.core import * 25 25 from trac.env import IEnvironmentSetupParticipant 26 from trac.mimeview.api import * 26 27 from trac.ticket import Milestone, Ticket, TicketSystem, ITicketManipulator 27 28 from trac.ticket.notification import TicketNotifyEmail 28 29 from trac.Timeline import ITimelineEventProvider … … 33 34 from trac.web import IRequestHandler 34 35 from trac.web.chrome import add_link, add_stylesheet, INavigationContributor 35 36 from trac.wiki import wiki_to_html, wiki_to_oneliner 36 from trac.mimeview.api import Mimeview, IContentConverter37 37 38 38 39 39 class InvalidTicket(TracError): … … 211 211 212 212 # IContentConverter methods 213 213 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) 221 219 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 223 223 if key == 'csv': 224 return self.export_csv(ticket, mimetype='text/csv')224 data = self.export_csv(ticket) 225 225 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') 228 227 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()) 230 231 231 232 # INavigationContributor methods 232 233 … … 254 255 255 256 ticket = Ticket(self.env, id, db=db) 256 257 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 257 265 if req.method == 'POST': 258 266 if not req.args.has_key('preview'): 259 267 self._do_save(req, db, ticket) … … 281 289 self._insert_ticket_data(req, db, ticket, 282 290 get_reporter_id(req, 'author')) 283 291 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 290 292 # If the ticket is being shown in the context of a query, add 291 293 # links to help navigate in the query result set 292 294 if 'query_tickets' in req.session: … … 308 310 add_stylesheet(req, 'common/css/ticket.css') 309 311 310 312 # Add registered converters 311 for c onversion 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 c onversion[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) 315 317 316 318 return 'ticket.cs', None 317 319 … … 429 431 430 432 # Internal methods 431 433 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 443 444 def export_rss(self, req, ticket): 444 445 db = self.env.get_db_cnx() 445 446 changes = [] … … 471 472 change['title'] = '; '.join(['%s %s' % (', '.join(v), k) for k, v \ 472 473 in change_summary.iteritems()]) 473 474 req.hdf['ticket.changes'] = changes 474 return (req.hdf.render('ticket_rss.cs'), 'application/rss+xml')475 return req.hdf.render('ticket_rss.cs') 475 476 476 477 477 def _do_save(self, req, db, ticket): 478 478 if req.perm.has_permission('TICKET_CHGPROP'): 479 479 # TICKET_CHGPROP gives permission to edit the ticket -
trac/ticket/tests/conversion.py
1 1 from trac.test import EnvironmentStub, Mock 2 from trac.util import sorted 2 from trac.util import sorted, CRLF 3 3 from trac.ticket.model import Ticket 4 4 from trac.ticket.web_ui import TicketModule 5 from trac.mimeview.api import Mimeview5 from trac.mimeview.api import * 6 6 from trac.web.clearsilver import HDFWrapper 7 7 from trac.web.href import Href 8 8 … … 14 14 def setUp(self): 15 15 self.env = EnvironmentStub() 16 16 self.ticket_module = TicketModule(self.env) 17 self.mimeview = Mimeview(self.env)18 17 self.req = Mock(hdf=HDFWrapper(['./templates']), 19 18 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')) 21 21 22 def _create_a_ticket (self):22 def _create_a_ticket_content(self): 23 23 # 1. Creating ticket 24 24 ticket = Ticket(self.env) 25 25 ticket['reporter'] = 'santa' 26 26 ticket['summary'] = 'Foo' 27 27 ticket['description'] = 'Bar' 28 28 ticket['foo'] = 'This is a custom field' 29 return ticket 29 # 2. Creating ObjectContent wrapper 30 return ObjectContent(self.env, ticket) 30 31 31 32 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)) 45 42 46 43 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 ''] 53 48 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) 54 54 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 55 62 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 ''] 63 67 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 64 82 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><p>\nBar\n</p>\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><p>\r', 91 'Bar\r', 92 '</p>\r', 93 '</description>', 94 ' <language>en-us</language>', 95 ' <generator>Trac v</generator>', 96 ' </channel>', 97 '</rss>', 98 ''] 76 99 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) 77 106 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 78 114 def suite(): 79 115 return unittest.makeSuite(TicketConversionTestCase, 'test') 80 116 -
trac/ticket/roadmap.py
184 184 185 185 # Internal methods 186 186 187 def render_ics(self, req, db, milestones): 187 def render_ics(self, req, db, milestones): # FIXME: use IContentConverter 188 188 req.send_response(200) 189 189 req.send_header('Content-Type', 'text/calendar;charset=utf-8') 190 190 req.end_headers() -
trac/ticket/query.py
21 21 22 22 from trac.core import * 23 23 from trac.db import get_column_names 24 from trac.mimeview.api import * 24 25 from trac.perm import IPermissionRequestor 25 26 from trac.ticket import Ticket, TicketSystem 26 27 from trac.util.datefmt import format_datetime, http_date … … 31 32 INavigationContributor 32 33 from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider 33 34 from trac.wiki.macros import WikiMacroBase 34 from trac.mimeview.api import Mimeview, IContentConverter35 35 36 36 class QuerySyntaxError(Exception): 37 37 """Exception raised when a ticket query cannot be parsed from a string.""" … … 351 351 IContentConverter) 352 352 353 353 # 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) 361 359 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 363 364 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) 367 366 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()) 369 372 370 373 # INavigationContributor methods 371 374 … … 413 416 del req.session[var] 414 417 req.redirect(query.get_href(req)) 415 418 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? ;) 422 424 423 425 constraints = {} 424 426 for k, v in query.constraints.items(): … … 435 437 constraints[k] = constraint 436 438 req.hdf['query.constraints'] = constraints 437 439 438 format = req.args.get('format')439 if format:440 Mimeview(self.env).send_converted(req, 'trac.ticket.Query', query,441 format, 'query')442 443 440 self.display_html(req, query) 444 441 return 'query.cs', None 445 442 … … 615 612 self.env.is_component_enabled(ReportModule): 616 613 req.hdf['query.report_href'] = req.href.report() 617 614 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() 620 622 cols = query.get_columns() 621 c ontent.write(sep.join([col for col in cols]) + CRLF)623 csv.write(sep.join([col for col in cols]) + CRLF) 622 624 623 results = query.execute(req, self.env.get_db_cnx())624 for result in results:625 c ontent.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', ' ') 628 630 for col in cols]) + CRLF) 629 return (content.getvalue(), '%s;charset=utf-8' % mimetype)631 return csv.getvalue() 630 632 631 633 def export_rss(self, req, query): 632 634 query.verbose = True … … 648 650 groupdesc=query.groupdesc and 1 or None, 649 651 verbose=query.verbose and 1 or None, 650 652 **query.constraints) 651 return (req.hdf.render('query_rss.cs'), 'application/rss+xml')653 return req.hdf.render('query_rss.cs') 652 654 653 655 # IWikiSyntaxProvider methods 654 656 -
trac/versioncontrol/web_ui/util.py
16 16 # Author: Jonas Borgström <jonas@edgewall.com> 17 17 # Christian Boos <cboos@neuf.fr> 18 18 19 import posixpath 19 20 import re 20 21 import urllib 21 22 22 23 from trac.core import TracError 24 from trac.mimeview.api import MimeContent 23 25 from trac.util.datefmt import format_datetime, pretty_timedelta 24 26 from trac.util.html import escape, html, Markup 25 27 from trac.util.text import shorten_line … … 27 29 from trac.wiki import wiki_to_html, wiki_to_oneliner 28 30 29 31 __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'] 31 34 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 42 CHUNK_SIZE = 4096 43 44 class 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 32 71 def get_changes(env, repos, revs, full=None, req=None, format=None): 33 72 db = env.get_db_cnx() 34 73 changes = {} -
trac/versioncontrol/web_ui/changeset.py
26 26 from trac import util 27 27 from trac.config import BoolOption, IntOption 28 28 from trac.core import * 29 from trac.mimeview import Mimeview, is_binary30 29 from trac.perm import IPermissionRequestor 31 30 from trac.Search import ISearchSource, search_to_sql, shorten_result 32 31 from trac.Timeline import ITimelineEventProvider … … 36 35 from trac.versioncontrol import Changeset, Node 37 36 from trac.versioncontrol.diff import get_diff_options, hdf_diff, unified_diff 38 37 from trac.versioncontrol.svn_authz import SubversionAuthorizer 39 from trac.versioncontrol.web_ui.util import render_node_property38 from trac.versioncontrol.web_ui.util import * 40 39 from trac.web import IRequestHandler 41 40 from trac.web.chrome import INavigationContributor, add_link, add_stylesheet 42 41 from trac.wiki import wiki_to_html, wiki_to_oneliner, IWikiSyntaxProvider, \ … … 439 438 The list is empty when no differences between comparable files 440 439 are detected, but the return value is None for non-comparable files. 441 440 """ 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: 444 443 return None 445 444 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: 448 447 return None 449 448 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]+') ? 453 459 454 460 if old_content != new_content: 455 461 context = 3 … … 526 532 'filename=%s.diff' % filename) 527 533 req.end_headers() 528 534 529 mimeview = Mimeview(self.env)530 535 for old_node, new_node, kind, change in repos.get_changes(**diff): 531 536 # TODO: Property changes 532 537 … … 536 541 537 542 new_content = old_content = '' 538 543 new_node_info = old_node_info = ('','') 539 mimeview = Mimeview(self.env)540 544 541 545 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: 544 548 continue 545 549 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) 548 551 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: 551 554 continue 552 555 new_node_info = (new_node.path, new_node.rev) 553 556 new_path = new_node.path 554 new_content = mimeview.to_unicode(new_content, 555 new_node.content_type) 557 new_content = unicode(new_content) 556 558 else: 557 559 old_node_path = repos.normalize_path(old_node.path) 558 560 diff_old_path = repos.normalize_path(diff.old_path) -
trac/versioncontrol/web_ui/browser.py
23 23 from trac import util 24 24 from trac.config import ListOption, Option 25 25 from trac.core import * 26 from trac.mimeview import Mimeview, is_binary, get_mimetype26 from trac.mimeview import Mimeview, MimeType, TEXT_PLAIN 27 27 from trac.perm import IPermissionRequestor 28 28 from trac.util import sorted, embedded_numbers 29 from trac.util.datefmt import http_date,format_datetime, pretty_timedelta29 from trac.util.datefmt import format_datetime, pretty_timedelta 30 30 from trac.util.html import escape, html, Markup 31 31 from trac.util.text import pretty_size 32 32 from trac.web import IRequestHandler, RequestDone … … 36 36 from trac.versioncontrol.web_ui.util import * 37 37 38 38 39 CHUNK_SIZE = 409640 41 42 39 class BrowserModule(Component): 43 40 44 41 implements(INavigationContributor, IPermissionRequestor, IRequestHandler, … … 199 196 def _render_file(self, req, repos, node, rev=None): 200 197 req.perm.assert_permission('FILE_VIEW') 201 198 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')) 212 202 # Eventually send the file directly 213 203 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) 227 209 else: 228 210 # The changeset corresponding to the last change on `node` 229 211 # is more interesting than the `rev` changeset. 230 changeset = repos.get_changeset(node. rev)212 changeset = repos.get_changeset(node.created_rev) 231 213 232 214 message = changeset.message or '--' 233 215 if self.config['changeset'].getbool('wiki_format_messages'): … … 238 220 239 221 req.hdf['file'] = { 240 222 'rev': node.rev, 241 'changeset_href': req.href.changeset(node. rev),223 'changeset_href': req.href.changeset(node.created_rev), 242 224 'date': format_datetime(changeset.date), 243 225 'age': pretty_timedelta(changeset.date), 244 226 'size': pretty_size(node.content_length), 245 227 'author': changeset.author or 'anonymous', 246 228 'message': message 247 229 } 248 230 249 231 # 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): 251 233 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) 254 236 255 237 # 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) 258 240 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']) 261 243 262 del content # the remainder of that content is not needed263 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 268 244 add_stylesheet(req, 'common/css/code.css') 269 245 270 246 # IWikiSyntaxProvider methods -
trac/wiki/web_ui.py
22 22 23 23 from trac.attachment import attachments_to_hdf, Attachment, AttachmentModule 24 24 from trac.core import * 25 from trac.mimeview.api import * 25 26 from trac.perm import IPermissionRequestor 26 27 from trac.Search import ISearchSource, search_to_sql, shorten_result 27 28 from trac.Timeline import ITimelineEventProvider … … 34 35 from trac.web import HTTPNotFound, IRequestHandler 35 36 from trac.wiki.api import IWikiPageManipulator, WikiSystem 36 37 from 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 38 from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner, \ 39 TEXT_X_TRAC_WIKI, APPLICATION_X_TRAC_WIKI 39 40 40 41 41 42 class InvalidWikiPage(TracError): … … 50 51 page_manipulators = ExtensionPoint(IWikiPageManipulator) 51 52 52 53 # IContentConverter methods 53 def get_supported_conversions(self):54 yield ('txt', 'Plain Text', 'txt', 'text/x-trac-wiki', 'text/plain', 9)55 54 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) 58 60 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 59 69 # INavigationContributor methods 60 70 61 71 def get_active_navigation_item(self, req): … … 129 139 else: 130 140 format = req.args.get('format') 131 141 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) 134 145 self._render_view(req, db, page) 135 146 136 147 req.hdf['wiki.action'] = action … … 430 441 req.hdf['html.norobots'] = 1 431 442 432 443 # Add registered converters 433 for c onversion 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 c onversion[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) 439 450 440 451 latest_page = WikiPage(self.env, page.name) 441 452 req.hdf['wiki'] = {'exists': page.exists, -
trac/wiki/formatter.py
31 31 from trac.util.text import shorten_line, to_unicode 32 32 33 33 __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'] 35 36 36 37 38 TEXT_X_TRAC_WIKI = MimeType('text/x-trac-wiki', 39 name='Trac Wiki Text', extension='txt') 40 41 APPLICATION_X_TRAC_WIKI = MimeType('application/x-trac-wiki', 42 name='Trac Wiki Text', extension='txt') 43 44 37 45 def system_message(msg, text=None): 38 46 return html.DIV(html.STRONG(msg), text and html.PRE(text), 39 47 class_="system-message") … … 43 51 44 52 _code_block_re = re.compile('^<div(?:\s+class="([^"]+)")?>(.*)</div>$') 45 53 46 def __init__(self, env, name):47 # TODO: transmit `formatter` argument48 self.env = env54 def __init__(self, formatter, name): 55 self.formatter = formatter 56 self.env = formatter.env 49 57 self.name = name 50 58 self.error = None 51 59 self.macro_provider = None 60 self.mimetype = None 52 61 53 62 builtin_processors = {'html': self._html_processor, 54 63 'default': self._default_processor, … … 59 68 # Find a matching wiki macro 60 69 for macro_provider in WikiSystem(self.env).macro_providers: 61 70 for macro_name in macro_provider.get_macros(): 62 if self.name == macro_name:71 if name == macro_name: 63 72 self.processor = self._macro_processor 64 73 self.macro_provider = macro_provider 65 74 break 66 75 if not self.processor: 67 76 # 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 72 80 self.processor = self._mimeview_processor 73 81 else: 74 82 self.processor = self._default_processor … … 94 102 # generic processors 95 103 96 104 def _macro_processor(self, req, text): 97 # TODO: macro should take a `formatter` argument 105 # TODO: macro should take a `formatter` argument (0.11) 98 106 self.env.log.debug('Executing Wiki macro %s by provider %s' 99 107 % (self.name, self.macro_provider)) 100 108 return self.macro_provider.render_macro(req, self.name, text) 101 109 102 110 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) 105 113 106 114 def process(self, req, text, in_paragraph=False): 107 115 if self.error: … … 157 165 ENDBLOCK_TOKEN = r"\}\}\}" 158 166 ENDBLOCK = "}}}" 159 167 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 161 170 INTERTRAC_SCHEME = r"[a-zA-Z.+-]*?" # no digits (support for shorthand links) 162 171 163 172 QUOTED_STRING = r"'[^']+'|\"[^\"]+\""
