Ticket #3332: mimeview_refactoring-typerepr-r3507.diff
| File mimeview_refactoring-typerepr-r3507.diff, 97.2 KB (added by cboos, 6 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 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.markup 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) … … 227 227 228 228 select = classmethod(select) 229 229 230 def open(self): 230 def open(self): # deprecate? 231 231 self.env.log.debug('Trying to open attachment at %s', self.path) 232 232 try: 233 233 fd = open(self.path, 'rb') … … 505 505 'author': get_reporter_id(req)} 506 506 507 507 def _render_view(self, req, attachment): 508 # FIXME: perm_map should extend to other `parent_type`s 508 509 perm_map = {'ticket': 'TICKET_VIEW', 'wiki': 'WIKI_VIEW'} 509 510 req.perm.assert_permission(perm_map[attachment.parent_type]) 510 511 … … 522 523 if req.perm.has_permission(perm_map[attachment.parent_type]): 523 524 req.hdf['attachment.can_delete'] = 1 524 525 525 fd = attachment.open() 526 try: 527 mimeview = Mimeview(self.env) 526 format = req.args.get('format') 527 raw_href = attachment.href(req, format='raw') 528 528 529 # MIME type detection 530 str_data = fd.read(1000) 531 fd.seek(0) 532 533 binary = is_binary(str_data) 534 mime_type = mimeview.get_mimetype(attachment.filename, str_data) 529 mimecontent = FileMimeContent(self.env, attachment.path, raw_href, 530 'Attachment') 531 can_render_as_txt = self.render_unsafe_content and \ 532 not mimecontent.is_binary 535 533 536 # Eventually send the file directly 537 format = req.args.get('format') 538 if format in ('raw', 'txt'): 539 if not self.render_unsafe_content and not binary: 540 # Force browser to download HTML/SVG/etc pages that may 541 # contain malicious code enabling XSS attacks 542 req.send_header('Content-Disposition', 'attachment;' + 543 'filename=' + attachment.filename) 544 if not mime_type or (self.render_unsafe_content and \ 545 not binary and format == 'txt'): 546 mime_type = 'text/plain' 547 if 'charset=' not in mime_type: 548 charset = mimeview.get_charset(str_data, mime_type) 549 mime_type = mime_type + '; charset=' + charset 550 req.send_file(attachment.path, mime_type) 534 # Eventually send the file directly 535 if format in ('raw', 'txt'): 536 if not self.render_unsafe_content and not mimecontent.is_binary: 537 # Force browser to download HTML/SVG/etc pages that may 538 # contain malicious code enabling XSS attacks 539 req.send_header('Content-Disposition', 540 'attachment;filename=%s' % attachment.filename) 541 if not mimecontent.type.is_known or \ 542 (format == 'txt' and can_render_as_txt): 543 # Force the content to be displayed as text 544 type = MimeType('text/plain', mimecontent.encoding) 545 else: 546 type = mimecontent.type 547 req.send_file(attachment.path, type.mimetype_charset) 551 548 552 # add ''Plain Text'' alternate link if needed 553 if self.render_unsafe_content and not binary and \ 554 mime_type and not mime_type.startswith('text/plain'): 555 plaintext_href = attachment.href(req, format='txt') 556 add_link(req, 'alternate', plaintext_href, 'Plain Text', 557 mime_type) 549 mimetype = mimecontent.type.mimetype 558 550 559 # add ''Original Format'' alternate link (always) 560 raw_href = attachment.href(req, format='raw') 561 add_link(req, 'alternate', raw_href, 'Original Format', mime_type) 551 # add ''Plain Text'' alternate link if needed 552 if can_render_as_txt and mimecontent.type.is_known and \ 553 mimetype != 'text/plain': 554 add_link(req, 'alternate', attachment.href(req, format='txt'), 555 'Plain Text', 'text/plain') 562 556 563 self.log.debug("Rendering preview of file %s with mime-type %s"564 % (attachment.filename, mime_type))557 # add ''Original Format'' alternate link (always) 558 add_link(req, 'alternate', raw_href, 'Original Format', mimetype) 565 559 566 req.hdf['attachment'] = mimeview.preview_to_hdf( 567 req, fd, os.fstat(fd.fileno()).st_size, mime_type, 568 attachment.filename, raw_href, annotations=['lineno']) 569 finally: 570 fd.close() 560 self.log.debug("Rendering preview of file %s with mime-type %s" % \ 561 (attachment.filename, mimetype)) 571 562 563 req.hdf['attachment'] = Mimeview(self.env).preview_to_hdf( 564 req, mimecontent, annotations=['lineno']) 565 572 566 def _render_list(self, req, p_type, p_id): 573 567 self._parent_to_hdf(req, p_type, p_id) 574 568 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_unicode31 from trac.mimeview.api import IHTMLPreviewRenderer 32 32 from trac.web.href import Href 33 33 from trac.wiki.formatter import WikiProcessor 34 34 from trac.wiki import WikiSystem … … 223 223 224 224 _inliner = rst.states.Inliner() 225 225 _parser = rst.Parser(inliner=_inliner) 226 content = content_to_unicode(self.env, content, mimetype)226 content = unicode(content) 227 227 content = content.encode('utf-8') 228 228 html = publish_string(content, writer_name='html', parser=_parser, 229 229 settings_overrides={'halt_level': 6}) -
trac/mimeview/api.py
19 19 # Christian Boos <cboos@neuf.fr> 20 20 21 21 """ 22 The `trac.mimeview` module centralize the intelligence related to23 file metadata, principally concerning the `type` (MIME type) of the content24 and, if relevant, concerning the text encoding (charset) used by the content.22 The `trac.mimeview` module centralizes the intelligence related to file 23 metadata, principally concerning the `type` (MIME type) and the text 24 encoding (charset) used by the content, if the latter one is relevant. 25 25 26 26 There are primarily two approaches for getting the MIME type of a given file: 27 27 * taking advantage of existing conventions for the file name 28 28 * examining the file content and applying various heuristics 29 29 30 The module also knows how to convert the file content from one type31 to another type.30 The module also knows about conversions from one data type to another type, 31 like conversions to text/html (this is no more a special case). 32 32 33 In some cases, only the `url` pointing to the file's content is actually 34 needed, that's why we avoid to read the file's content when it's not needed. 35 36 The actual `content` to be converted might be a `unicode` object, 37 but it can also be the raw byte string (`str`) object, or simply 38 an object that can be `read()`. 33 In order to keep the API simple, we deal with a few classes, 34 each encapsulating a part of the knowledge related to content. 35 * the `MimeType`, for storing the mime type string, the charset, 36 but also eventually the name and the commonly used file extension 37 for that type 38 * the `MimeContent` and `FileMimeContent`, which provide a flexible 39 API for handling string and file content, respectively. 40 Those classes inherit from the abstract `MimeContentBase`, which 41 can be used to make new wrapper classes for any kind of content. 42 * the `Conversion` class, which is use to specify conversion from 43 one `MimeType` to another 39 44 """ 40 45 46 import os 41 47 import re 42 48 from StringIO import StringIO 43 49 44 50 from trac.config import IntOption, ListOption, Option 45 51 from trac.core import * 46 52 from trac.util import sorted 47 from trac.util.text import to_utf8, to_unicode 53 from trac.util.text import to_utf8, to_unicode, IDENTITY_CHARSET 48 54 from trac.util.markup import escape, Markup, Fragment, html 49 55 50 56 51 __all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 'Mimeview', 52 'content_to_unicode'] 57 __all__ = ['get_mimetype', 'is_binary', 'detect_unicode', 58 'Mimeview', 'Conversion', 'MimeContentBase', 'MimeType', 59 'MimeContent', 'FileMimeContent', 60 'TypeWrapper', 'ObjectContent', 'IContentConverter', 61 'TEXT_PLAIN', 'TEXT_HTML', 62 'APPLICATION_RSS_XML', 'APPLICATION_OCTET_STREAM'] 53 63 54 64 55 65 # Some common MIME types and their associated keywords and/or file extensions 56 66 67 APPLICATION_OCTET_STREAM_STR = 'application/octet-stream' 68 57 69 KNOWN_MIME_TYPES = { 58 70 'application/pdf': ['pdf'], 59 71 'application/postscript': ['ps'], … … 114 126 for e in exts: 115 127 MIME_MAP[e] = t 116 128 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 129 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. 130 # -- a few functions for dealing with MIME types / binary / text content 131 # in a simple way (get_mimetype, is_binary, detect_unicode) 126 132 133 def get_mimetype_from_filename(filename, mime_map=MIME_MAP): 134 """Guess the most probable MIME type of file with the given `filename`. 135 127 136 `filename` is either a filename (the lookup will then use the suffix) 128 137 or some arbitrary keyword. 129 130 `content` is either a `str` or an `unicode` string. 138 `mime_map` maps keywords to MIME types. 139 140 Return the MIME type as a string, or `None` if not detected. 131 141 """ 132 142 suffix = filename.split('.')[-1] 133 143 if suffix in mime_map: … … 141 151 mimetype = mimetypes.guess_type(filename)[0] 142 152 except: 143 153 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 154 return mimetype 157 155 156 # Simple builtin autodetection from the content using a regexp 157 MODE_RE = re.compile( 158 r"#!(?:[/\w.-_]+/)?(\w+)|" # look for shebang 159 r"-\*-\s*(?:mode:\s*)?([\w+-]+)\s*-\*-|" # look for Emacs' -*- mode -*- 160 r"vim:.*?syntax=(\w+)" # look for VIM's syntax=<n> 161 ) 162 163 def get_mimetype_from_content(content): 164 """Guess the most probable MIME type of file with the given `filename`. 165 166 `content` is either a `str` or an `unicode` string 167 168 Return the MIME type as a string, or `None` if not detected. 169 """ 170 match = re.search(MODE_RE, content[:1000]) 171 if match: 172 mode = match.group(1) or match.group(3) or \ 173 match.group(2).lower() 174 if mode in mime_map: 175 # 3) mimetype from the content, using the `MODE_RE` 176 return mime_map[mode] 177 else: 178 if is_binary(content): 179 # 4) mimetype from the content, using `is_binary` 180 return APPLICATION_OCTET_STREAM_STR 181 182 def get_mimetype(filename, content=None, mime_map=MIME_MAP): 183 """Auto-detect MIME type either from the `filename` or from the `content`. 184 """ 185 mimetype = get_mimetype_from_filename(filename, mime_map) 186 if not mimetype and content: 187 mimetype = get_mimetype_from_content(content) 188 return mimetype 189 158 190 def is_binary(data): 159 191 """Detect binary content by checking the first thousand bytes for zeroes. 160 192 … … 165 197 return '\0' in data[:1000] 166 198 167 199 def detect_unicode(data): 168 """Detect different unicode charsets by looking for B OMs (Byte Order Marks).200 """Detect different unicode charsets by looking for Byte Order Marks. 169 201 170 202 Operate obviously only on `str` objects. 171 203 """ … … 178 210 else: 179 211 return None 180 212 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 213 214 # -- Classes for mimetype, content and conversion 188 215 216 class TypeRepr(object): 217 """Represent some for of typing.""" 218 def match(self, other, regexp=False): 219 raise NotImplementedError 220 221 mimetype = property(lambda x: x._mimetype or APPLICATION_OCTET_STREAM_STR, 222 doc="MIME Type string (without charset information)") 223 charset = property(lambda x: x._charset, 224 doc="Eventual charset information") 225 is_binary = property(lambda x: x._is_binary()) 226 227 class TypeWrapper(TypeRepr): 228 """Typing using Python types.""" 229 230 def __init__(self, obj): 231 self.class_ = isinstance(obj, type) and obj or obj.__class__ 232 self._mimetype = self._charset = None 233 234 def __repr__(self): 235 return self.class_.__name__ 236 237 def _is_binary(self): 238 return True 239 240 def match(self, other, regexp=False): 241 other_class = isinstance(other, type) and other or \ 242 isinstance(other, TypeWrapper) and other.class_ or \ 243 other.__class__ 244 return other_class == self.class_ 245 246 class MimeType(TypeRepr): 247 """Typing of arbitrary content using MIME types. 248 249 If the MIME type correspond to text content, the object can also 250 store a `charset` information. 251 252 A MIME type has a `name` and has an `extension` 253 that can be used for storing the converted data in a file. 254 255 All the properties of this class are read-only. 256 """ 257 258 def __init__(self, mimetype, charset=None, name=None, extension=None): 259 """The `mimetype` string can eventually embed the `charset`.""" 260 self._mimetype = mimetype 261 # determine charset 262 self._charset = charset 263 if not self._charset and self._mimetype: 264 sep_idx = mimetype.find(';') 265 if sep_idx >= 0: 266 self._mimetype = mimetype[:sep_idx].strip() 267 charset_idx = mimetype.find('charset=', sep_idx) 268 if charset_idx >= 0: 269 self._charset = mimetype[charset_idx+8:].strip() 270 self._extension = extension 271 self._name = name 272 273 def __repr__(self): 274 return 'MIME type: ' + self.mimetype_charset 275 276 def _get_extension(self): 277 if not self._extension: 278 self._extension = KNOWN_MIME_TYPES.get(self.mimetype) 279 if not self._extension: 280 detail = self.mimetype.split('/', 1)[1] 281 if detail.startswith('x-'): 282 self._extension = detail[2:] 283 return self._extension 284 285 def _get_mimetype_charset(self): 286 """Combine the MIME type and charset information in a single string. 287 """ 288 if self._mimetype and self._charset: 289 return '%s; charset=%s' % (self.mimetype, self.charset) 290 else: 291 return self.mimetype 292 293 def _is_binary(self): 294 return self._mimetype == APPLICATION_OCTET_STREAM_STR 295 296 name = property(lambda x: x._name or x._extension) 297 extension = property(lambda x: x._get_extension()) 298 mimetype_charset = property(lambda x: x._get_mimetype_charset()) 299 300 def match(self, other, regexp=False): 301 """Compare MIME type string only. 302 303 If `regexp` is set, `self.mimetype` is used as a regexp. 304 """ 305 if not isinstance(other, MimeType): 306 return False 307 if regexp: 308 return re.match(self.mimetype, other.mimetype) 309 else: 310 return self.mimetype == other.mimetype 311 312 313 TEXT_PLAIN = MimeType('text/plain', 'utf-8', 'Plain Text', 'txt') 314 TEXT_HTML = MimeType('text/html', 'utf-8', 'HTML', 'html') 315 316 APPLICATION_RSS_XML = MimeType('application/rss+xml', 'utf-8', 317 'RSS Feed', 'xml') 318 APPLICATION_OCTET_STREAM = MimeType(APPLICATION_OCTET_STREAM_STR, 319 IDENTITY_CHARSET, 320 'Undefined (binary)', 'bin') 321 322 323 class MimeContentBase(object): 324 """An abstract MIME content, with an associated MimeType. 325 326 Such an object has means to auto-detect both the MIME Type and 327 the `encoding` of its content. 328 329 That `encoding` is more reliable than the `type.charset` information. 330 There are additional consistency checks that are performed, and it 331 can be `None` if the content is an `unicode` object. 332 333 The content itself can be accessed in various ways: through 334 the iterator protocol, the len() and unicode() operators... 335 """ 336 337 def __init__(self, env, mimetype=None, filename=None, url=None): 338 """ 339 `mimetype` can be specified as a `MimeType` object, 340 or as string, which will then be a hint about the content. 341 342 If the mimetype is not specified or equal to 343 "application/octet-stream", then it will be auto-detected when needed. 344 345 In case auto-detection fails, APPLICATION_OCTET_STREAM will be the 346 corresponding MIME type. 347 348 The `filename` is simply a suggested basename for that content. 349 350 The `url` is a link for retrieving the raw content directly 351 from the server. This can be useful for converters that can 352 provide links to objects, instead of having to expand the 353 content inline. 354 """ 355 self.env = env 356 if isinstance(mimetype, basestring): 357 mimetype = MimeType(mimetype) 358 self._type = mimetype 359 self._filename = filename 360 self._url = url 361 self._binary = None 362 self._encoding = False 363 364 def __repr__(self): 365 return '<%s %s "%s">' % (self.__class__.__name__, self._type, 366 self._filename or self._url) 367 368 def __unicode__(self): 369 """Return the `unicode` object corresponding to the content.""" 370 return to_unicode(self.content, self.encoding) 371 # Note: this does the right thing if the content is already `unicode` 372 373 def encode(self, charset): 374 """Return a `str`, corresponding to the `charset` encoded content.""" 375 if self.encoding == charset: 376 return self.content 377 else: 378 return unicode(self).encode(charset) 379 380 def _is_binary(self): 381 """An heuristic for guessing whether the content is binary or not. 382 383 This will eventually fetch an `excerpt` of the content. 384 """ 385 if self._binary is None: 386 self._binary = self.type.is_binary 387 print `self.type`, self._binary 388 if self._binary is None: 389 self._binary = is_binary(self.excerpt) or \ 390 (self.type.mimetype in \ 391 Mimeview(self.env).treat_as_binary) 392 return self._binary 393 394 def _get_type(self): 395 """Get or determine the MimeType corresponding to this content. 396 397 An `excerpt` of the content will be examined if needed. 398 """ 399 if self._type is None: # not set 400 mimetype = None 401 if self.filename: 402 mimemap = Mimeview(self.env).mimemap 403 mimetype = get_mimetype_from_filename(self.filename, mimemap) 404 if not mimetype: 405 mimetype = get_mimetype_from_content(self.excerpt) 406 if not mimetype: 407 pass # TODO 0.11: go through IMimeTypeDetectors 408 self._type = MimeType(mimetype) 409 return self._type 410 411 def _set_type(self, type): 412 """Simply replace the existing `type` by the given `MimeType` object. 413 414 If `None` is given, this will force auto-detection the next time 415 `type` will be accessed. 416 417 Can be used for in-place conversion (e.g. ''any'' to text/plain). 418 """ 419 self._type = type 420 421 def _get_encoding(self): 422 """Get or determine the current encoding of that `content`. 423 424 The encoding will be determined using this order: 425 * from the charset information present in the mimetype information 426 * auto-detection of the charset from the `content` 427 * if nothing else worked, use the configured `default_charset` 428 429 If the `content` happens to be a genuine `unicode` object, then 430 this returns `None`. 431 If the `content` is binary, then the encoding will be the identity 432 charset (ISO Latin 1). 433 """ 434 if self._encoding is False: 435 charset = self.type.charset 436 if charset: 437 self._encoding = charset 438 elif isinstance(self.excerpt, str): 439 utf_encoding = detect_unicode(self.excerpt) 440 if utf_encoding is not None: 441 self._encoding = utf_encoding 442 elif self.is_binary: 443 self._encoding = IDENTITY_CHARSET 444 elif isinstance(self.excerpt, unicode): 445 self._encoding = None 446 if self._encoding is False: 447 pass # TODO 0.11: go through ICharsetDetectors here 448 if self._encoding is False: 449 self._encoding = Mimeview(self.env).default_charset 450 return self._encoding 451 452 def _get_content(self): 453 """Retrieve all the content. 454 455 Default implementation based on iterator. If the iterator itself 456 is implemented based on the content... reimplement this one! 457 """ 458 return "".join(self.__iter__()) 459 460 def read(self): # TODO: remove in 0.11 461 return self.content # (compatibility with IHTMLPreviewRenderer) 462 463 # Methods that need to be reimplemented by subclasses: 464 465 def __iter__(self): 466 """Iterate on chunks of raw content.""" 467 raise NotImplementedError 468 469 def __len__(self): 470 """Length of the raw content, in bytes.""" 471 raise NotImplementedError 472 473 def _get_excerpt(self, len=1000): 474 """Extracts the first `len` characters from the content.""" 475 raise NotImplementedError 476 477 type = property(fget=lambda x: x._get_type(), 478 fset=lambda x, y: x._set_type(y)) 479 is_binary = property(lambda x: x._is_binary()) 480 encoding = property(lambda x: x._get_encoding()) 481 excerpt = property(lambda x: x._get_excerpt()) 482 content = property(lambda x: x._get_content()) 483 filename = property(lambda x: x._filename) 484 url = property(lambda x: x._url) 485 486 487 class MimeContent(MimeContentBase): 488 """MIME-typed content wrapper for a basestring.""" 489 490 def __init__(self, env, content, mimetype, filename='file', url=None): 491 MimeContentBase.__init__(self, env, mimetype, filename, url) 492 self._content = content 493 494 # Reimplemented methods 495 496 def _get_content(self): 497 """Retrieve the wrapped content. 498 499 Note: therefore this *might* be an `unicode` object. 500 Remember that in this case, `encoding` will be `None`. 501 """ 502 return self._content 503 504 def __iter__(self): 505 """Iterate on chunks of content. 506 507 If the content `is_binary` property is `False`, those chunks will 508 be lines, with the line endings kept. 509 """ 510 if self.is_binary: 511 buf = StringIO(self.content) 512 chunk = buf.read(1000) 513 while chunk: 514 yield chunk 515 chunk = buf.read(1000) 516 else: 517 for line in self.content.splitlines(True): 518 yield line 519 520 def __len__(self): 521 """Length of the content, in characters.""" 522 return len(self.content) 523 524 def _get_excerpt(self, len=1000): 525 """Extracts the first `len` characters from the content.""" 526 return self._content[:len] 527 528 529 class FileMimeContent(MimeContentBase): 530 """MIME-typed content wrapper for a file.""" 531 532 def __init__(self, env, path, url=None, kind='File', mimetype=None): 533 self._fd = None 534 self._path = path 535 self._kind = kind 536 self._excerpt = None 537 MimeContentBase.__init__(self, env, mimetype, os.path.basename(path), 538 url) 539 def __del__(self): 540 if self._fd: 541 self._fd.close() 542 543 def _ensure_open(self): 544 if not self._fd: 545 try: 546 self._fd = open(self._path) 547 except IOError: 548 raise TracError('%s "%s" not found' % (self._kind, 549 self._filename)) 550 # Reimplemented methods 551 552 def __iter__(self): 553 """Iterate on chunks of raw content.""" 554 chunk = self.excerpt 555 while chunk: 556 yield chunk 557 chunk = self._fd.read(1000) 558 559 def __len__(self): 560 """Length of the raw content, in bytes.""" 561 if self._fd: 562 stat = os.fstat(self._fd.fileno()) 563 else: 564 stat = os.stat(self._path) 565 return stat.st_size 566 567 def _get_excerpt(self, len=1000): 568 """Extracts the `len` first bytes from the content.""" 569 if self._excerpt is None: 570 self._ensure_open() 571 self._excerpt = self._fd.read(1000) 572 return self._excerpt 573 574 class ObjectContent(MimeContentBase): 575 """Wraps a Python object into a MimeContentBase. 576 577 Only supports the bare minimum of the MimeContentBase methods. 578 """ 579 580 def __init__(self, env, obj, filename="obj"): 581 self._obj = obj 582 MimeContentBase.__init__(self, env, TypeWrapper(obj), 583 filename=filename) 584 585 def _get_content(self): 586 """Retrieve the wrapped content.""" 587 return self._obj 588 589 590 class NoConversion(TracError): 591 def __init__(self, msg, from_, output, key): 592 TracError.__init__(self, '%s, from %s to %s' % 593 (msg, repr(from_), output and repr(output) or key)) 594 595 class Conversion(object): 596 """A specification for performing a data conversion. 597 598 Each conversion is identified by a `key` and targets an output `mimetype`. 599 600 A conversion also specifies a `quality` ranking, which is a number 601 in the range 0 to 9, where 0 means no support and 9 means "perfect" 602 support (try to keep 9 available for user defined conversions, 603 though nothing will prevent them from using 10 or 100...) 604 605 Finally, `expand_tabs` indicates whether a tab expansion should precede 606 the conversion attempt. 607 608 e.g. Conversion(key='latex', quality=8, mimetype=MimeType('text/x-tex')) 609 """ 610 611 def __init__(self, key, quality=1, mimetype=TEXT_HTML, expand_tabs=False): 612 self.key = key 613 self.quality = quality 614 self.mimetype = mimetype 615 self.expand_tabs = expand_tabs 616 617 def __repr__(self): 618 return '<%s conversion to %s [qr=%s, et=%s]>' % \ 619 (self.key, self.mimetype, self.quality, self.expand_tabs) 620 621 622 # -- Deprecated (TODO: remove in 0.11) 623 189 624 class IHTMLPreviewRenderer(Interface): 190 625 """Extension point interface for components that add HTML renderers of 191 626 specific content types to the `Mimeview` component. 192 627 193 (Deprecated)628 Deprecated in 0.10. Implement `IContentConverter` instead. 194 629 """ 195 630 196 631 # implementing classes should set this property to True if they … … 198 633 expand_tabs = False 199 634 200 635 def get_quality_ratio(mimetype): 201 """Return the level of support this renderer provides for the `content` 202 of the specified MIME type. The return value must be a number between 203 0 and 9, where 0 means no support and 9 means "perfect" support. 204 """ 636 """Return the level of support this renderer provides""" 205 637 206 638 def render(req, mimetype, content, filename=None, url=None): 207 """Render an XHTML preview of the raw `content`. 639 """Render an XHTML preview of the raw `content`.""" 208 640 209 The `content` might be:210 * a `str` object211 * an `unicode` string212 * any object with a `read` method, returning one of the above213 641 214 It is assumed that the content will correspond to the given `mimetype`. 642 # -- Interfaces for the extension points 215 643 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.220 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 will223 be considered to correspond to lines of text in the original content.644 class IContentConverter(Interface): 645 """An extension point interface for generic content conversion.""" 646 647 def get_supported_conversions(mimetype): 648 """Check if conversion of `mimetype` is supported by this converter. 649 650 Return an iterable of `Conversion` objects for which this is 651 the case. 224 652 """ 225 653 654 def convert_content(context, conversion, content): 655 """Convert the given `content` using the specified `conversion`. 656 657 The conversion takes place in the given formatting `context`. 658 A `context` provides at least a `req` property. 659 660 Return the converted content as a new `MimeContent` object. 661 """ 662 226 663 class IHTMLPreviewAnnotator(Interface): 227 664 """Extension point interface for components that can annotate an XHTML 228 665 representation of file contents with additional information.""" … … 240 677 annotation data.""" 241 678 242 679 243 class IContentConverter(Interface): 244 """An extension point interface for generic MIME based content 245 conversion.""" 680 # -- The main Mimeview component 246 681 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)""" 682 class ToplevelContext(object): 683 """A simple wrapper for the Request object. 254 684 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."""685 Use this when no other context information is available. 686 """ 687 def __init__(self, req): 688 self.req = req 259 689 260 261 690 class Mimeview(Component): 262 691 """A generic class to prettify data, typically source code.""" 263 692 264 renderers = ExtensionPoint(IHTMLPreviewRenderer) 693 renderers = ExtensionPoint(IHTMLPreviewRenderer) # TODO: remove in 0.11 265 694 annotators = ExtensionPoint(IHTMLPreviewAnnotator) 266 695 converters = ExtensionPoint(IContentConverter) 267 696 … … 275 704 """Maximum file size for HTML preview. (''since 0.9'').""") 276 705 277 706 mime_map = ListOption('mimeviewer', 'mime_map', 278 'text/x-dylan:dylan,text/x-idl:ice,text/x-ada:ads:adb', 707 'text/x-dylan:dylan,text/x-idl:ice,text/x-ada:ads:adb', doc= 279 708 """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 709 710 Mappings are comma-separated. Each mapping starts with the mimetype, 711 followed by a colon (":") and the (colon separated) list of associated 712 keywords or file extensions. (''since 0.10'').""") 713 714 treat_as_binary = ListOption('mimeviewer', 'treat_as_binary', 715 'application/pdf,application/postscript,application/rtf', doc= 716 """List of MIME types that should always be treated as binary content. 717 718 Accounts for the fact that our binary detection heuristic can't 719 always work for some kind of binary data. (''since 0.10'').""") 720 284 721 def __init__(self): 285 722 self._mime_map = None 286 287 # Public API288 723 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.""" 724 def _get_mimemap(self): 725 """Extend default extension to MIME type mappings""" 726 if not self._mime_map: 727 self._mime_map = {} 728 self._mime_map.update(MIME_MAP) 729 for mapping in self.config['mimeviewer'].getlist('mime_map'): 730 if ':' in mapping: 731 assocations = mapping.split(':') 732 mimetype = assocations[0] 733 for keyword in assocations: # mimetype->mimetype on purpose 734 self._mime_map[keyword] = mimetype 735 return self._mime_map 736 737 mimemap = property(_get_mimemap) 738 739 def lookup(self, keyword, charset=None): 740 """Lookup for given `keyword`, among known MIME Types. 741 742 Return a `MimeType` object if found, `None` otherwise. 743 """ 744 mimetype = self.mimemap.get(keyword, None) 745 if mimetype: 746 return MimeType(mimetype, charset, extension=keyword) 747 748 # -- MIME type conversion 749 750 def get_conversions(self, input): 751 """Return a list of possible conversions for the `input` MimeType. 752 753 The returned list contains pair of `(conversion, converter)` objects, 754 ordered from best to worst quality. 755 """ 756 # Build list of possible conversions 293 757 converters = [] 294 758 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 759 for conversion in converter.get_supported_conversions(input): 760 if conversion.quality > 0: 761 converters.append((conversion, converter)) 300 762 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') 763 # ---- Backward compatibility support for IHTMLPreviewRenderer 764 class IHTMLPreviewRendererWrapper(object): 765 def __init__(self, renderer): 766 self.renderer = renderer 767 def __repr__(self): 768 return repr(self.renderer) 769 def convert_content(self, context, conversion, mimecontent): 770 return self.renderer.render( 771 context.req, mimecontent.type.mimetype, 772 mimecontent, # which is read()able 773 mimecontent.filename, mimecontent.url) 774 for renderer in self.renderers: 775 qr = renderer.get_quality_ratio(input.mimetype) 776 if qr > 0: 777 expand_tabs = getattr(renderer, 'expand_tabs', False) 778 converters.append( 779 (Conversion(key='', quality=qr, mimetype=TEXT_HTML, 780 expand_tabs=expand_tabs), 781 IHTMLPreviewRendererWrapper(renderer))) 782 # ---- (to be removed in 0.11) 308 783 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 784 return sorted(converters, key=lambda c: c[0].quality, reverse=True) 785 786 def convert(self, context, selector, mimecontent): 787 """Convert the `mimecontent` to another MIME type. 788 789 The conversion to be done is determined by `selector`, 790 which can be either directly the desired output MIME type or 791 a key identifying the `Conversion` object. 792 793 Returns a new `MimeContent`. 794 """ 795 result, mimetype = self._convert(context, selector, mimecontent) 796 797 if isinstance(result, MimeContentBase): 798 return result 799 else: 800 return MimeContent(self.env, result, mimetype, 801 filename=mimecontent.filename) 802 803 def _convert(self, context, selector, mimecontent): 804 """Convert the `mimecontent` to another MIME type. 805 806 Doesn't necessarily return a new `MimeContent`: can be a basestring, 807 a Fragment, an iterable... 808 809 """ 810 # See whether we've got a type selector 811 if isinstance(selector, TypeRepr): 812 selected_output = selector 813 selected_key = None 317 814 else: 318 mimetype = full_mimetype = 'text/plain' # fallback if not binary 815 selected_output = self.lookup(selector) 816 if selected_output: 817 selected_key = None 818 else: 819 selected_key = selector 319 820 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])] 821 # Get all available conversions for the input and filter those which 822 # are matching either the `selected_output` or the `selected_key` 823 candidates = [] 824 for cc_pair in self.get_conversions(mimecontent.type): 825 conversion = cc_pair[0] 826 if selected_key == conversion.key or \ 827 conversion.mimetype.match(selected_output): ### TODO: conversion.type 828 candidates.append(cc_pair) 323 829 if not candidates: 324 raise TracError('No available MIME conversions from %s to %s' %325 (mimetype, key))830 raise NoConversion('No available MIME conversions', 831 mimecontent.type, selected_output, selected_key) 326 832 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)) 833 tab_expanded = None # we don't want to expand tabs more than once. 336 834 835 # First candidate which converts successfully wins. 836 for conversion, converter in candidates: 837 self.log.debug(u'Attempting conversion of %s using %s and %s' % 838 (mimecontent, conversion, converter)) 839 if conversion.expand_tabs and not tab_expanded: 840 tab_expanded = unicode(mimecontent).expandtabs(self.tab_width) 841 mimecontent = MimeContent(self.env, tab_expanded, 842 mimecontent.type) 843 self.log.debug('tab expansion performed.') 844 try: 845 res = converter.convert_content(context, conversion, 846 mimecontent) 847 if res: 848 return res, conversion.mimetype 849 except Exception, e: 850 self.log.warning('MIME conversion using %s failed (%s)' 851 % (converter, e), exc_info=True) 852 raise NoConversion('No MIME conversions succeeded', 853 mimecontent.type, selected_output, selected_key) 854 855 # -- XHTML rendering and annotations (based on the conversion API) 856 337 857 def get_annotation_types(self): 338 858 """Generator that returns all available annotation types.""" 339 859 for annotator in self.annotators: 340 860 yield annotator.get_annotation_type() 341 861 342 def render(self, req, mimetype, content, filename=None, url=None, 343 annotations=None): 344 """Render an XHTML preview of the given `content`. 862 def render(self, req, mimecontent, annotations=None): 863 """Render an XHTML preview of the given `mimecontent`. 345 864 346 `content` is the same as an `IHTMLPreviewRenderer.render`'s 347 `content` argument. 348 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. 353 354 Return a string containing the XHTML text. 865 Some `annotations` might be requested as well. 355 866 """ 356 if not content: 357 return '' 867 result, _ = self._convert(ToplevelContext(req), 'text/html', 868 mimecontent) 869 if isinstance(result, Fragment): 870 return result # might be processed further 871 elif isinstance(result, basestring): 872 self.log.warning('HTML rendering: got %s' % 873 result.__class__.__name__) 874 return Markup(to_unicode(result)) # needed for compatibility 358 875 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 876 # otherwise, it's an iterable yielding lines 877 if annotations: 878 return Markup(self._annotate(result, annotations)) 369 879 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])) 880 return html.DIV(html.PRE(Markup(''.join(result))), class_="code") 377 881 378 # First candidate which renders successfully wins.379 # Also, we don't want to expand tabs more than once.380 expanded_content = None381 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 expansion386 rendered_content = content387 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_content393 result = renderer.render(req, full_mimetype, rendered_content,394 filename, url)395 if not result:396 continue397 elif isinstance(result, Fragment):398 return result399 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)413 414 882 def _annotate(self, lines, annotations): 883 """Add requested `annotations` to the lines' content.""" 415 884 buf = StringIO() 416 885 buf.write('<table class="code"><thead><tr>') 417 886 annotators = [] … … 445 914 buf.write('</tbody></table>') 446 915 return buf.getvalue() 447 916 917 # -- Deprecated API (TODO: remove in 0.11) 918 448 919 def get_max_preview_size(self): 449 920 """Deprecated: use `max_preview_size` attribute directly.""" 450 921 return self.max_preview_size 451 922 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 923 def to_utf8(self, content, mimetype=None): 499 924 """Convert an encoded `content` to utf-8. 500 925 501 926 ''Deprecated in 0.10. You should use `unicode` strings only.'' 502 927 """ 503 return to_utf8(content , self.get_charset(content, mimetype))928 return to_utf8(content) 504 929 505 def to_unicode(self, content, mimetype=None, charset=None): 506 """Convert `content` (an encoded `str` object) to an `unicode` object. 930 # -- Utilities 507 931 508 This calls `trac.util.to_unicode` with the `charset` provided, 509 or the one obtained by `Mimeview.get_charset()`. 510 """ 511 if not charset: 512 charset = self.get_charset(content, mimetype) 513 return to_unicode(content, charset) 932 def is_binary(self, typerepr): 933 """Checks whether a given `TypeRepr` is binary or not.""" 934 return typerepr.is_binary or typerepr.mimetype in self.treat_as_binary 514 935 515 936 def configured_modes_mapping(self, renderer): 516 """Return a MIME type to `(mode,quality)` mapping for given `option`""" 937 """Utility for configurable custom converters 938 939 Return a MIME type to `(mode,quality)` mapping for given `option`, 940 assuming a format of comma-separated <mimetype>:<mode>:<quality> 941 associations. 942 943 See EnscriptConverter and SilverCityConverter. 944 """ 517 945 types, option = {}, '%s_modes' % renderer 518 946 for mapping in self.config['mimeviewer'].getlist(option): 519 947 if not mapping: … … 526 954 "option." % (mapping, option)) 527 955 return types 528 956 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`. 532 533 Note: `content` will usually be an object with a `read` method. 534 """ 535 if length >= self.max_preview_size: 957 def preview_to_hdf(self, req, mimecontent, annotations=None): 958 """Prepares a rendered preview of the given `mimecontent`.""" 959 if len(mimecontent) >= self.max_preview_size: 536 960 return {'max_file_size_reached': True, 537 961 'max_file_size': self.max_preview_size, 538 'raw_href': url}962 'raw_href': mimecontent.url} 539 963 else: 540 return {'preview': self.render(req, mimetype, content, filename, 541 url, annotations), 542 'raw_href': url} 964 try: 965 preview = self.render(req, mimecontent, annotations) 966 except NoConversion, e: 967 preview = None 968 return {'preview': preview, 969 'raw_href': mimecontent.url} 543 970 544 def send_converted(self, req, in_type, content, selector, filename='file'):545 """Helper method for converting ` content` and sending it directly.971 def send_converted(self, req, selector, mimecontent): 972 """Helper method for converting `mimecontent` and sending it directly. 546 973 547 `selector` can be either a key or a MIME Type.""" 974 `selector` can be either a key or the expected output MIME Type. 975 """ 548 976 from trac.web import RequestDone 549 content, output_type, ext = self.convert_content(req, in_type, 550 content, selector) 977 result = self.convert(ToplevelContext(req), selector, mimecontent) 551 978 req.send_response(200) 552 req.send_header('Content-Type', output_type)553 req.send_header('Content-Disposition', 'filename=%s.%s' % (filename,554 ext))979 req.send_header('Content-Type', result.type.mimetype_charset) 980 req.send_header('Content-Disposition', 'filename=%s.%s' % 981 (result.filename, result.type.extension)) 555 982 req.end_headers() 556 req.write(content) 557 raise RequestDone 558 983 req.write(result.encode('utf-8')) 984 raise RequestDone 559 985 986 987 988 # utility for Mimeview._annotate 560 989 def _html_splitlines(lines): 561 990 """Tracks open and close tags in lines of HTML text and yields lines that 562 991 have no tags spanning more than one line.""" … … 604 1033 def annotate_line(self, number, content): 605 1034 return '<th id="L%s"><a href="#L%s">%s</a></th>' % (number, number, 606 1035 number) 1036 # return html.TH(html.A(number, href="#L%s" % number), id=number) 607 1037 608 1038 609 # -- Default renderers1039 # -- Default TEXT_HTML converters (previously ''IHTMLPreviewRenderer'') 610 1040 611 1041 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. 1042 """Convert text to HTML-escaped text. 1043 1044 Will be used as a fallback for any kind of text 1045 for which no more specific HTML converter is available. 614 1046 """ 615 implements(I HTMLPreviewRenderer)1047 implements(IContentConverter) 616 1048 617 expand_tabs = True 618 619 TREAT_AS_BINARY = [ 620 'application/pdf', 621 'application/postscript', 622 'application/rtf' 623 ] 624 625 def get_quality_ratio(self, mimetype): 626 if mimetype in self.TREAT_AS_BINARY: 627 return 0 628 return 1 629 630 def render(self, req, mimetype, content, filename=None, url=None): 631 if is_binary(content): 1049 def get_supported_conversions(self, input): 1050 if Mimeview(self.env).is_binary(input): 1051 return 1052 yield Conversion(key='default', 1053 quality=TEXT_PLAIN.match(input) and 8 or 1, 1054 mimetype=TEXT_HTML, expand_tabs=True) 1055 1056 def convert_content(self, context, conversion, mimecontent): 1057 if mimecontent.is_binary: 632 1058 self.env.log.debug("Binary data; no preview available") 633 return 1059 else: 1060 if not TEXT_PLAIN.match(mimecontent.type): 1061 self.env.log.debug("Fallback to plain text renderer.") 1062 mimecontent.type = MimeType('text/plain', mimecontent.encoding) 1063 return mimecontent 634 1064 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)639 1065 640 641 1066 class ImageRenderer(Component): 642 """Inline image display. Here we don't need the `content` at all.""" 643 implements(IHTMLPreviewRenderer) 1067 """Inline image display. 644 1068 645 def get_quality_ratio(self, mimetype): 646 if mimetype.startswith('image/'): 647 return 8 648 return 0 1069 This renderer doesn't need the actual data at all, only the url. 1070 """ 1071 implements(IContentConverter) 649 1072 650 def render(self, req, mimetype, content, filename=None, url=None): 651 if url: 652 return html.DIV(html.IMG(src=url,alt=filename), 1073 def get_supported_conversions(self, input): 1074 if MimeType('^image/').match(input, regexp=True): 1075 yield Conversion(key='image', quality=8, mimetype=TEXT_HTML) 1076 1077 def convert_content(self, context, conversion, mimecontent): 1078 if mimecontent.url: 1079 return html.DIV(html.IMG(src=mimecontent.url, 1080 alt=mimecontent.filename), 653 1081 class_="image-file") 654 1082 655 1083 656 1084 class WikiTextRenderer(Component): 657 1085 """Render files containing Trac's own Wiki formatting markup.""" 658 implements(IHTMLPreviewRenderer)659 1086 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 1087 implements(IContentConverter) 664 1088 665 def render(self, req, mimetype, content, filename=None, url=None): 1089 def get_supported_conversions(self, input): 1090 from trac.wiki import TEXT_X_TRAC_WIKI, APPLICATION_X_TRAC_WIKI 1091 if TEXT_X_TRAC_WIKI.match(input) or \ 1092 APPLICATION_X_TRAC_WIKI.match(input): 1093 yield Conversion(key='wiki', quality=8, mimetype=TEXT_HTML) 1094 1095 def convert_content(self, context, conversion, mimecontent): 666 1096 from trac.wiki import wiki_to_html 667 return wiki_to_html(content_to_unicode(self.env, content, mimetype), 668 self.env, req) 1097 return MimeContent(self.env, wiki_to_html(unicode(mimecontent), 1098 self.env, context.req), 1099 TEXT_HTML) -
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, MimeContent, \ 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 88 self._types.update( 88 89 Mimeview(self.env).configured_modes_mapping('silvercity')) 89 return self._types.get(mimetype, (None, 0))[1] 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, mimecontent): 92 95 import SilverCity 93 96 try: 94 mimetype = mimetype.split(';', 1)[0] 95 typelang = self._types[mimetype] 97 typelang = self._types[mimecontent.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 = mimecontent.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 MimeContent(self.env, html.replace(' ', ' '), 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.markup 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.markup 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): 40 40 """Exception raised when a ticket fails validation.""" 41 41 42 42 43 TEXT_CSV = MimeType('text/csv', 'utf-8', 44 'Comma-delimited Text', 'csv') 45 TEXT_TSV = MimeType('text/tab-separated-values', 'utf-8', 46 'Tab-delimited Text', 'tsv') 47 48 43 49 class TicketModuleBase(Component): 44 50 # FIXME: temporary place-holder for unified ticket validation until 45 51 # ticket controller unification is merged … … 211 217 212 218 # IContentConverter methods 213 219 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) 220 def get_supported_conversions(self, input): 221 if input.match(Ticket): 222 yield Conversion('csv', 8, TEXT_CSV) 223 yield Conversion('tab', 8, TEXT_TSV) 224 yield Conversion('rss', 8, APPLICATION_RSS_XML) 221 225 222 def convert_content(self, req, mimetype, ticket, key): 226 def convert_content(self, context, conversion, objcontent): 227 key = conversion.key 223 228 if key == 'csv': 224 return self.export_csv( ticket, mimetype='text/csv')229 return self.export_csv(objcontent, TEXT_CSV) 225 230 elif key == 'tab': 226 return self.export_csv(ticket, sep='\t', 227 mimetype='text/tab-separated-values') 231 return self.export_csv(objcontent, TEXT_TSV, sep='\t') 228 232 elif key == 'rss': 229 return self.export_rss( req, ticket)233 return self.export_rss(context.req, objcontent) 230 234 231 235 # INavigationContributor methods 232 236 … … 281 285 self._insert_ticket_data(req, db, ticket, 282 286 get_reporter_id(req, 'author')) 283 287 284 mime = Mimeview(self.env)285 288 format = req.args.get('format') 286 289 if format: 287 mime.send_converted(req, 'trac.ticket.Ticket', ticket, format, 288 'ticket_%d' % ticket.id) 290 Mimeview(self.env).send_converted( 291 req, format, ObjectContent(self.env, ticket, 292 filename='ticket_%d' % ticket.id)) 289 293 290 294 # If the ticket is being shown in the context of a query, add 291 295 # links to help navigate in the query result set … … 308 312 add_stylesheet(req, 'common/css/ticket.css') 309 313 310 314 # Add registered converters 311 for conv ersion in mime.get_supported_conversions('trac.ticket.Ticket'):312 conversion_href = req.href.ticket(ticket.id, format=conv ersion[0])313 add_link(req, 'alternate', conversion_href, conversion[1],314 conv ersion[3])315 for conv, _ in Mimeview(self.env).get_conversions(TypeWrapper(Ticket)): 316 conversion_href = req.href.ticket(ticket.id, format=conv.key) 317 add_link(req, 'alternate', conversion_href, 318 conv.mimetype.name, conv.mimetype.mimetype_charset) 315 319 316 320 return 'ticket.cs', None 317 321 … … 429 433 430 434 # Internal methods 431 435 432 def export_csv(self, ticket, sep=',', mimetype='text/plain'): 436 def export_csv(self, objcontent, mimetype, sep=','): 437 ticket = objcontent.content 433 438 content = StringIO() 434 439 content.write(sep.join(['id'] + [f['name'] for f in ticket.fields]) 435 440 + CRLF) … … 438 443 .replace(sep, '_').replace('\\', '\\\\') 439 444 .replace('\n', '\\n').replace('\r', '\\r') 440 445 for f in ticket.fields]) + CRLF) 441 return (content.getvalue(), '%s;charset=utf-8' % mimetype) 446 return MimeContent(self.env, content.getvalue(), mimetype, 447 filename=objcontent.filename) 442 448 443 def export_rss(self, req, ticket): 449 def export_rss(self, req, objcontent): 450 ticket = objcontent.content 444 451 db = self.env.get_db_cnx() 445 452 changes = [] 446 453 change_summary = {} … … 471 478 change['title'] = '; '.join(['%s %s' % (', '.join(v), k) for k, v \ 472 479 in change_summary.iteritems()]) 473 480 req.hdf['ticket.changes'] = changes 474 return (req.hdf.render('ticket_rss.cs'), 'application/rss+xml') 481 return MimeContent(self.env, req.hdf.render('ticket_rss.cs'), 482 APPLICATION_RSS_XML, filename=objcontent.filename) 475 483 476 477 484 def _do_save(self, req, db, ticket): 478 485 if req.perm.has_permission('TICKET_CHGPROP'): 479 486 # TICKET_CHGPROP gives permission to edit the ticket -
trac/ticket/tests/conversion.py
2 2 from trac.util import sorted 3 3 from trac.ticket.model import Ticket 4 4 from trac.ticket.web_ui import TicketModule 5 from trac.mimeview.api import Mimeview 5 from trac.mimeview.api import Mimeview, Conversion 6 6 from trac.web.clearsilver import HDFWrapper 7 7 from trac.web.href import Href 8 8 … … 17 17 self.mimeview = Mimeview(self.env) 18 18 self.req = Mock(hdf=HDFWrapper(['./templates']), 19 19 base_path='/trac.cgi', path_info='', 20 href=Href('/trac.cgi')) 20 href=Href('/trac.cgi'), 21 abs_href=Href('http://example.org/trac.cgi')) 21 22 22 23 def _create_a_ticket(self): 23 24 # 1. Creating ticket … … 29 30 return 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, 33 conversions = self.mimeview\ 34 .get_supported_conversions('trac.ticket.Ticket') 35 expected = sorted([(Conversion('csv', 'Comma-delimited Text', 'csv', 36 'trac.ticket.Ticket', 'text/csv', 8), 37 self.ticket_module), 38 (Conversion('tab', 'Tab-delimited Text', 'tsv', 39 'trac.ticket.Ticket', 40 'text/tab-separated-values', 8), 41 self.ticket_module), 42 (Conversion('rss', 'RSS Feed', 'xml', 43 'trac.ticket.Ticket', 44 'application/rss+xml', 8), 42 45 self.ticket_module)], 43 46 key=lambda i: i[-1], reverse=True) 44 self.assertEqual(expected, conversions) 47 for expected, actual in zip(expected, conversions): 48 self.assertEqual(expected[1], actual[1]) 49 for attr in ('key', 'name', 'extension', 'in_type', 'out_type', 50 'quality', 'expand_tabs'): 51 self.assertEqual(getattr(expected[0], attr), 52 getattr(actual[0], attr)) 45 53 46 54 def test_csv_conversion(self): 47 55 ticket = self._create_a_ticket() 48 csv = self.mimeview.convert_content(self.req, 'trac.ticket.Ticket', 49 ticket, 'csv') 56 csv = self.mimeview.convert_content(self.req, 57 ticket, 'trac.ticket.Ticket', 58 'csv') 50 59 self.assertEqual((u'id,summary,reporter,owner,description,keywords,cc' 51 60 '\r\nNone,Foo,santa,,Bar,,\r\n', 52 61 'text/csv;charset=utf-8', 'csv'), csv) … … 54 63 55 64 def test_tab_conversion(self): 56 65 ticket = self._create_a_ticket() 57 csv = self.mimeview.convert_content(self.req, 'trac.ticket.Ticket', 58 ticket, 'tab') 66 csv = self.mimeview.convert_content(self.req, 67 ticket, 'trac.ticket.Ticket', 68 'tab') 59 69 self.assertEqual((u'id\tsummary\treporter\towner\tdescription\tkeywords' 60 70 '\tcc\r\nNone\tFoo\tsanta\t\tBar\t\t\r\n', 61 71 'text/tab-separated-values;charset=utf-8', 'tsv'), … … 64 74 def test_rss_conversion(self): 65 75 ticket = self._create_a_ticket() 66 76 content, mimetype, ext = self.mimeview.convert_content( 67 self.req, 'trac.ticket.Ticket', ticket, 'rss')77 self.req, ticket, 'trac.ticket.Ticket', 'rss') 68 78 self.assertEqual(('<?xml version="1.0"?>\n<!-- RSS generated by Trac v ' 69 79 'on -->\n<rss version="2.0">\n <channel>\n ' 70 80 '<title>Ticket </title>\n <link></link>\n ' -
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 MimeContentBase 23 25 from trac.util.datefmt import format_datetime, pretty_timedelta 24 26 from trac.util.text import shorten_line 25 27 from trac.util.markup import escape, html, Markup … … 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(MimeContentBase): 45 """Encapsulation of the content of a file in the repository.""" 46 47 def __init__(self, env, node, url=None): 48 self.node = node 49 MimeContentBase.__init__(self, env, node.content_type, 50 posixpath.basename(node.path), url) 51 self._excerpt = None 52 53 def __iter__(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._content.read(CHUNK_SIZE) 62 63 def __len__(self): 64 return self.node.get_content_length() 65 66 def _get_excerpt(self): 67 if not self._excerpt: 68 self._content = self.node.get_content() 69 self._excerpt = self._content.read(CHUNK_SIZE) 70 return self._excerpt 71 72 32 73 def get_changes(env, repos, revs, full=None, req=None, format=None): 33 74 db = env.get_db_cnx() 34 75 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_content) 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) 453 451 454 452 if old_content != new_content: 455 453 context = 3 … … 526 524 'filename=%s.diff' % filename) 527 525 req.end_headers() 528 526 529 mimeview = Mimeview(self.env)530 527 for old_node, new_node, kind, change in repos.get_changes(**diff): 531 528 # TODO: Property changes 532 529 … … 536 533 537 534 new_content = old_content = '' 538 535 new_node_info = old_node_info = ('','') 539 mimeview = Mimeview(self.env)540 536 541 537 if old_node: 542 old_content = old_node.get_content().read()543 if is_binary(old_content):538 old_content = NodeMimeContent(self.env, old_node) 539 if old_content.is_binary: 544 540 continue 545 541 old_node_info = (old_node.path, old_node.rev) 546 old_content = mimeview.to_unicode(old_content, 547 old_node.content_type) 542 old_content = unicode(old_content) 548 543 if new_node: 549 new_content = new_node.get_content().read()550 if is_binary(new_content):544 new_content = NodeMimeContent(self.env, new_node) 545 if new_content.is_binary: 551 546 continue 552 547 new_node_info = (new_node.path, new_node.rev) 553 548 new_path = new_node.path 554 new_content = mimeview.to_unicode(new_content, 555 new_node.content_type) 549 new_content = unicode(new_content) 556 550 else: 557 551 old_node_path = repos.normalize_path(old_node.path) 558 552 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 27 27 from trac.perm import IPermissionRequestor 28 28 from trac.util import sorted, embedded_numbers 29 29 from trac.util.datefmt import http_date, format_datetime, pretty_timedelta … … 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)199 format = req.args.get('format') 203 200 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' 201 raw_href = req.href.browser(node.path, rev=rev, format='raw') 211 202 203 mimecontent = NodeMimeContent(self.env, node, raw_href) 204 212 205 # Eventually send the file directly 213 format = req.args.get('format') 214 if format in ['raw', 'txt']: 206 if format in ('raw', 'txt'): 207 if format == 'txt': 208 type = MimeType('text/plain', mimecontent.encoding) 209 else: 210 type = mimecontent.type 215 211 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) 212 req.send_header('Content-Type', type.mimetype_charset) 213 req.send_header('Content-Length', len(mimecontent)) 219 214 req.send_header('Last-Modified', http_date(node.last_modified)) 220 215 req.end_headers() 221 216 222 while 1: 223 if not chunk: 224 raise RequestDone 217 for chunk in mimecontent: 225 218 req.write(chunk) 226 chunk = content.read(CHUNK_SIZE)219 raise RequestDone 227 220 else: 228 221 # The changeset corresponding to the last change on `node` 229 222 # is more interesting than the `rev` changeset. 230 changeset = repos.get_changeset(node. rev)223 changeset = repos.get_changeset(node.created_rev) 231 224 232 225 message = changeset.message or '--' 233 226 if self.config['changeset'].getbool('wiki_format_messages'): … … 238 231 239 232 req.hdf['file'] = { 240 233 'rev': node.rev, 241 'changeset_href': req.href.changeset(node. rev),234 'changeset_href': req.href.changeset(node.created_rev), 242 235 'date': format_datetime(changeset.date), 243 236 'age': pretty_timedelta(changeset.date), 244 237 'size': pretty_size(node.content_length), … … 246 239 'message': message 247 240 } 248 241 242 mimetype = mimecontent.type.mimetype 243 249 244 # add ''Plain Text'' alternate link if needed 250 if not is_binary(chunk) and mime_type != 'text/plain':245 if not mimecontent.is_binary and mimetype != 'text/plain': 251 246 plain_href = req.href.browser(node.path, rev=rev, format='txt') 252 247 add_link(req, 'alternate', plain_href, 'Plain Text', 253 248 'text/plain') 254 249 255 250 # 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) 251 add_link(req, 'alternate', raw_href, 'Original Format', mimetype) 258 252 259 253 self.log.debug("Rendering preview of node %s@%s with mime-type %s" 260 % (node.name, str(rev), mime _type))254 % (node.name, str(rev), mimetype)) 261 255 262 del content # the remainder of that content is not needed 256 req.hdf['file'] = Mimeview(self.env).preview_to_hdf( 257 req, mimecontent, annotations=['lineno']) 263 258 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 259 add_stylesheet(req, 'common/css/code.css') 269 260 270 261 # IWikiSyntaxProvider methods -
trac/wiki/web_ui.py
34 34 from trac.web import HTTPNotFound, IRequestHandler 35 35 from trac.wiki.api import IWikiPageManipulator, WikiSystem 36 36 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 37 from trac.wiki.formatter import wiki_to_html, wiki_to_oneliner, \ 38 TEXT_X_TRAC_WIKI, APPLICATION_X_TRAC_WIKI 39 from trac.mimeview.api import Mimeview, IContentConverter, Conversion, \ 40 MimeContent, TEXT_PLAIN 39 41 40 42 41 43 class InvalidWikiPage(TracError): … … 50 52 page_manipulators = ExtensionPoint(IWikiPageManipulator) 51 53 52 54 # IContentConverter methods 53 def get_supported_conversions(self):54 yield ('txt', 'Plain Text', 'txt', 'text/x-trac-wiki', 'text/plain', 9)55 55 56 def convert_content(self, req, mimetype, content, key): 57 return (content, 'text/plain;charset=utf-8') 56 def get_supported_conversions(self, input): 57 if TEXT_X_TRAC_WIKI.match(input) or \ 58 APPLICATION_X_TRAC_WIKI.match(input): 59 yield Conversion('txt', quality=8, mimetype=TEXT_PLAIN) 58 60 61 def convert_content(self, context, conversion, content): 62 return content # identity transform 63 59 64 # INavigationContributor methods 60 65 61 66 def get_active_navigation_item(self, req): … … 128 133 else: 129 134 format = req.args.get('format') 130 135 if format: 131 Mimeview(self.env).send_converted(req, 'text/x-trac-wiki', 132 page.text, format, page.name) 136 wiki_content = MimeContent(self.env, page.text, 137 TEXT_X_TRAC_WIKI, 138 filename=page.name) 139 Mimeview(self.env).send_converted(req, format, wiki_content) 133 140 self._render_view(req, db, page) 134 141 135 142 req.hdf['wiki.action'] = action … … 429 436 req.hdf['html.norobots'] = 1 430 437 431 438 # Add registered converters 432 for conversion in Mimeview(self.env).get_supported_conversions( 433 'text/x-trac-wiki'): 439 for conv, _ in Mimeview(self.env).get_conversions(TEXT_X_TRAC_WIKI): 440 if conv.key in ('wiki', 'default'): 441 continue 434 442 conversion_href = req.href.wiki(page.name, version=version, 435 format=conv ersion[0])436 add_link(req, 'alternate', conversion_href, conversion[1],437 conv ersion[3])443 format=conv.key) 444 add_link(req, 'alternate', conversion_href, 445 conv.mimetype.name, conv.mimetype.mimetype_charset) 438 446 439 447 latest_page = WikiPage(self.env, page.name) 440 448 req.hdf['wiki'] = {'exists': page.exists, -
trac/wiki/formatter.py
31 31 from trac.util.markup import escape, Markup, Element, html 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, … … 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).lookup(self.name) 78 if not APPLICATION_OCTET_STREAM.match(mimetype): 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 class FormatterContext(object): 112 def __init__(self, req): 113 self.req = req 114 blockcontent = MimeContent(self.env, text, self.mimetype) 115 return Mimeview(self.env).convert(self.formatter, 'text/html', 116 blockcontent) 117 105 118 106 119 def process(self, req, text, in_paragraph=False): 107 120 if self.error: … … 429 442 return '<br />' 430 443 args = fullmatch.group('macroargs') 431 444 try: 432 macro = WikiProcessor(self .env, name)445 macro = WikiProcessor(self, name) 433 446 return macro.process(self.req, args, True) 434 447 except Exception, e: 435 448 self.env.log.error('Macro %s(%s) failed' % (name, args), … … 714 727 else: 715 728 self.code_text += line + os.linesep 716 729 if not self.code_processor: 717 self.code_processor = WikiProcessor(self .env, 'default')730 self.code_processor = WikiProcessor(self, 'default') 718 731 elif line.strip() == Formatter.ENDBLOCK: 719 732 self.in_code_block -= 1 720 733 if self.in_code_block == 0 and self.code_processor: … … 728 741 match = Formatter._processor_re.search(line) 729 742 if match: 730 743 name = match.group(1) 731 self.code_processor = WikiProcessor(self .env, name)744 self.code_processor = WikiProcessor(self, name) 732 745 else: 733 746 self.code_text += line + os.linesep 734 self.code_processor = WikiProcessor(self .env, 'default')747 self.code_processor = WikiProcessor(self, 'default') 735 748 else: 736 749 self.code_text += line + os.linesep 737 750 -
trac/web/chrome.py
17 17 import os 18 18 import re 19 19 20 from trac import mimeview21 20 from trac.config import * 22 21 from trac.core import * 23 22 from trac.env import IEnvironmentSetupParticipant 23 from trac.mimeview import get_mimetype 24 24 from trac.util.markup import html 25 25 from trac.web.api import IRequestHandler, HTTPNotFound 26 26 from trac.web.href import Href … … 288 288 icon = href.chrome(icon) 289 289 else: 290 290 icon = href.chrome('common', icon) 291 mimetype = mimeview.get_mimetype(icon)291 mimetype = get_mimetype(icon) 292 292 add_link(req, 'icon', icon, mimetype=mimetype) 293 293 add_link(req, 'shortcut icon', icon, mimetype=mimetype) 294 294 -
trac/util/text.py
25 25 26 26 27 27 CRLF = '\r\n' 28 IDENTITY_CHARSET = 'iso-8859-1' # not iso-8859-15 28 29 29 30 # -- Unicode 30 31 … … 40 41 If the `lossy` argument is `True`, which is the default, then 41 42 we use the 'replace' mode: 42 43 43 If the `lossy` argument is `False`, we fallback to the 'iso-8859-1 5'44 charset in case of an error (encoding a `str` using 'iso-8859-1 5'44 If the `lossy` argument is `False`, we fallback to the 'iso-8859-1' 45 charset in case of an error (encoding a `str` using 'iso-8859-1' 45 46 will always work, as there's one Unicode character for each byte of 46 47 the input). 47 48 """ … … 65 66 except UnicodeError: 66 67 return unicode(text, locale.getpreferredencoding(), errors) 67 68 except UnicodeError: 68 return unicode(text, 'iso-8859-15')69 return unicode(text, IDENTITY_CHARSET) 69 70 70 71 def unicode_quote(value): 71 72 """A unicode aware version of urllib.quote""" … … 85 86 return urlencode([(k, isinstance(v, unicode) and v.encode('utf-8') or v) 86 87 for k, v in params]) 87 88 88 def to_utf8(text, charset= 'iso-8859-15'):89 def to_utf8(text, charset=IDENTITY_CHARSET): 89 90 """Convert a string to UTF-8, assuming the encoding is either UTF-8, ISO 90 91 Latin-1, or as specified by the optional `charset` parameter. 91 92 … … 101 102 u = unicode(text, charset) 102 103 except UnicodeError: 103 104 # This should always work 104 u = unicode(text, 'iso-8859-15')105 u = unicode(text, IDENTITY_CHARSET) 105 106 return u.encode('utf-8') 106 107 107 108 -
templates/browser.cs
111 111 </table><?cs 112 112 /if ?><?cs 113 113 114 if:!browser.is_dir ?> 115 <div id="preview"><?cs 116 if:file.preview ?><?cs 117 var:file.preview ?><?cs 118 elif:file.max_file_size_reached ?> 119 <strong>HTML preview not available</strong>, since the file size exceeds 120 <?cs var:file.max_file_size ?> bytes. Try <a href="<?cs 121 var:file.raw_href ?>">downloading</a> the file instead.<?cs 122 else ?><strong>HTML preview not available</strong>. To view, <a href="<?cs 123 var:file.raw_href ?>">download</a> the file.<?cs 124 /if ?> 125 </div><?cs 114 if:!browser.is_dir ?><?cs 115 call:html_preview(file) ?><?cs 126 116 /if ?> 127 117 128 118 <div id="help"> -
templates/macros.cs
194 194 195 195 def:plural(base, count) ?><?cs 196 196 var:base ?><?cs if:count != 1 ?>s<?cs /if ?><?cs 197 /def ?><?cs 198 199 def:html_preview(base) ?> 200 <div id="preview"><?cs 201 if:base.preview ?> 202 <?cs var:base.preview ?><?cs 203 elif:base.max_file_size_reached ?> 204 <strong>HTML preview not available</strong>, since the file size exceeds 205 <?cs var:base.max_file_size ?> bytes. You may <a href="<?cs 206 var:base.raw_href ?>">download the file</a> instead.<?cs 207 else ?> 208 <strong>HTML preview not available</strong>. To view the file, 209 <a href="<?cs var:base.raw_href ?>">download the file</a>.<?cs 210 /if ?> 211 </div><?cs 197 212 /def ?> -
templates/attachment.cs
66 66 </th></tr><tr> 67 67 <td class="message"><?cs var:attachment.description ?></td> 68 68 </tr> 69 </tbody></table> 70 <div id="preview"><?cs 71 if:attachment.preview ?> 72 <?cs var:attachment.preview ?><?cs 73 elif:attachment.max_file_size_reached ?> 74 <strong>HTML preview not available</strong>, since the file size exceeds 75 <?cs var:attachment.max_file_size ?> bytes. You may <a href="<?cs 76 var:attachment.raw_href ?>">download the file</a> instead.<?cs 77 else ?> 78 <strong>HTML preview not available</strong>. To view the file, 79 <a href="<?cs var:attachment.raw_href ?>">download the file</a>.<?cs 80 /if ?> 81 </div> 69 </tbody></table><?cs 70 call:html_preview(attachment) ?> 82 71 <?cs if:attachment.can_delete ?><div class="buttons"> 83 72 <form method="get" action=""><div id="delete"> 84 73 <input type="hidden" name="action" value="delete" />
