📄 browser.py
字号:
# -*- coding: utf-8 -*-## Copyright (C) 2003-2008 Edgewall Software# Copyright (C) 2003-2005 Jonas Borgstr枚m <jonas@edgewall.com># Copyright (C) 2005-2007 Christian Boos <cboos@neuf.fr># All rights reserved.## This software is licensed as described in the file COPYING, which# you should have received as part of this distribution. The terms# are also available at http://trac.edgewall.org/wiki/TracLicense.## This software consists of voluntary contributions made by many# individuals. For the exact contribution history, see the revision# history and logs, available at http://trac.edgewall.org/log/.## Author: Jonas Borgstr枚m <jonas@edgewall.com>from datetime import datetime, timedeltafrom fnmatch import fnmatchcaseimport reimport osimport urllibfrom genshi.builder import tagfrom trac.config import ListOption, BoolOption, Optionfrom trac.core import *from trac.mimeview.api import Mimeview, is_binary, get_mimetype, \ IHTMLPreviewAnnotator, Contextfrom trac.perm import IPermissionRequestorfrom trac.resource import ResourceNotFound, Resourcefrom trac.util import sorted, embedded_numbersfrom trac.util.datefmt import http_date, utcfrom trac.util.html import escape, Markupfrom trac.util.text import shorten_linefrom trac.util.translation import _from trac.web import IRequestHandler, RequestDonefrom trac.web.chrome import add_ctxtnav, add_link, add_script, add_stylesheet, \ prevnext_nav, INavigationContributorfrom trac.wiki.api import IWikiSyntaxProviderfrom trac.wiki.formatter import format_to_html, format_to_onelinerfrom trac.versioncontrol.api import NoSuchChangeset, NoSuchNodefrom trac.versioncontrol.web_ui.util import *CHUNK_SIZE = 4096class IPropertyRenderer(Interface): """Render node properties in TracBrowser and TracChangeset views.""" def match_property(name, mode): """Indicate whether this renderer can treat the given property `mode` is the current rendering context, which can be: - 'browser' rendered in the browser view - 'changeset' rendered in the changeset view as a node property - 'revprop' rendered in the changeset view as a revision property Other identifiers might be used by plugins, so it's advised to simply ignore unknown modes. Returns a quality number, ranging from 0 (unsupported) to 9 (''perfect'' match). """ def render_property(name, mode, context, props): """Render the given property. `name` is the property name as given to `match()`, `mode` is the same as for `match_property`, `context` is the context for the node being render (useful when the rendering depends on the node kind) and `props` is the collection of the corresponding properties (i.e. the `node.get_properties()`). The rendered result can be one of the following: - `None`: the property will be skipped - an `unicode` value: the property will be displayed as text - a `RenderedProperty` instance: the property will only be displayed using the instance's `content` attribute, and the other attributes will also be used in some display contexts (like `revprop`) - `Markup` or other Genshi content: the property will be displayed normally, using that content as a block-level markup """class RenderedProperty(object): def __init__(self, name=None, name_attributes=None, content=None, content_attributes=None): self.name = name self.name_attributes = name_attributes self.content = content self.content_attributes = content_attributesclass DefaultPropertyRenderer(Component): """Implement default (pre-0.11) behavior for rendering properties.""" implements(IPropertyRenderer) hidden_properties = ListOption('browser', 'hide_properties', 'svk:merge', doc="""Comma-separated list of version control properties to hide from the repository browser. (''since 0.9'')""") def match_property(self, name, mode): # Support everything but hidden properties. return name not in self.hidden_properties and 1 or 0 def render_property(self, name, mode, context, props): # No special treatment besides respecting newlines in values. value = props[name] if value and '\n' in value: value = Markup(''.join(['<br />%s' % escape(v) for v in value.split('\n')])) return valueclass WikiPropertyRenderer(Component): """Render properties as wiki text.""" implements(IPropertyRenderer) wiki_properties = ListOption('browser', 'wiki_properties', 'trac:description', doc="""Comma-separated list of version control properties to render as wiki content in the repository browser. (''since 0.11'')""") oneliner_properties = ListOption('browser', 'oneliner_properties', 'trac:summary', doc="""Comma-separated list of version control properties to render as oneliner wiki content in the repository browser. (''since 0.11'')""") def match_property(self, name, mode): return (name in self.wiki_properties or \ name in self.oneliner_properties) and 4 or 0 def render_property(self, name, mode, context, props): if name in self.wiki_properties: return format_to_html(self.env, context, props[name]) else: return format_to_oneliner(self.env, context, props[name])class TimeRange(object): min = datetime(1, 1, 1, 0, 0, 0, 0, utc) # tz aware version of datetime.min def __init__(self, base): self.oldest = self.newest = base self._total = None def seconds_between(self, dt1, dt2): delta = dt1 - dt2 return delta.days * 24 * 3600 + delta.seconds def to_seconds(self, dt): return self.seconds_between(dt, TimeRange.min) def from_seconds(self, secs): return TimeRange.min + timedelta(*divmod(secs, 24* 3600)) def relative(self, datetime): if self._total is None: self._total = float(self.seconds_between(self.newest, self.oldest)) age = 1.0 if self._total: age = self.seconds_between(datetime, self.oldest) / self._total return age def insert(self, datetime): self._total = None self.oldest = min(self.oldest, datetime) self.newest = max(self.newest, datetime)class BrowserModule(Component): implements(INavigationContributor, IPermissionRequestor, IRequestHandler, IWikiSyntaxProvider, IHTMLPreviewAnnotator) property_renderers = ExtensionPoint(IPropertyRenderer) downloadable_paths = ListOption('browser', 'downloadable_paths', '/trunk, /branches/*, /tags/*', doc="""List of repository paths that can be downloaded. Leave the option empty if you want to disable all downloads, otherwise set it to a comma-separated list of authorized paths (those paths are glob patterns, i.e. "*" can be used as a wild card) (''since 0.10'')""") color_scale = BoolOption('browser', 'color_scale', True, doc="""Enable colorization of the ''age'' column. This uses the same color scale as the source code annotation: blue is older, red is newer. (''since 0.11'')""") NEWEST_COLOR = (255,136,136) newest_color = Option('browser', 'newest_color', repr(NEWEST_COLOR), doc="""(r,g,b) color triple to use for the color corresponding to the newest color, for the color scale used in ''blame'' or the browser ''age'' column if `color_scale` is enabled. (''since 0.11'')""") OLDEST_COLOR = (136,136,255) oldest_color = Option('browser', 'oldest_color', repr(OLDEST_COLOR), doc="""(r,g,b) color triple to use for the color corresponding to the oldest color, for the color scale used in ''blame'' or the browser ''age'' column if `color_scale` is enabled. (''since 0.11'')""") intermediate_point = Option('browser', 'intermediate_point', '', doc="""If set to a value between 0 and 1 (exclusive), this will be the point chosen to set the `intermediate_color` for interpolating the color value. (''since 0.11'')""") intermediate_color = Option('browser', 'intermediate_color', '', doc="""(r,g,b) color triple to use for the color corresponding to the intermediate color, if two linear interpolations are used for the color scale (see `intermediate_point`). If not set, the intermediate color between `oldest_color` and `newest_color` will be used. (''since 0.11'')""") render_unsafe_content = BoolOption('browser', 'render_unsafe_content', 'false', """Whether attachments should be rendered in the browser, or only made downloadable. Pretty much any file may be interpreted as HTML by the browser, which allows a malicious user to attach a file containing cross-site scripting attacks. For public sites where anonymous users can create attachments it is recommended to leave this option disabled (which is the default).""") # public methods def get_custom_colorizer(self): """Returns a converter for values from [0.0, 1.0] to a RGB triple.""" def interpolate(old, new, value): # Provides a linearly interpolated color triple for `value` # which must be a floating point value between 0.0 and 1.0 return tuple([int(b+(a-b)*value) for a,b in zip(new,old)]) def parse_color(rgb, default): # Get three ints out of a `rgb` string or return `default` try: t = tuple([int(v) for v in re.split(r'(\d+)', rgb)[1::2]]) return len(t) == 3 and t or default except ValueError: return default newest_color = parse_color(self.newest_color, self.NEWEST_COLOR) oldest_color = parse_color(self.oldest_color, self.OLDEST_COLOR) try: intermediate = float(self.intermediate_point) except ValueError: intermediate = None if intermediate: intermediate_color = parse_color(self.intermediate_color, None) if not intermediate_color: intermediate_color = tuple([(a+b)/2 for a,b in zip(newest_color, oldest_color)]) def colorizer(value): if value <= intermediate: value = value / intermediate return interpolate(oldest_color, intermediate_color, value) else: value = (value - intermediate) / (1.0 - intermediate) return interpolate(intermediate_color, newest_color, value) else: def colorizer(value): return interpolate(oldest_color, newest_color, value) return colorizer # INavigationContributor methods def get_active_navigation_item(self, req): return 'browser' def get_navigation_items(self, req): if 'BROWSER_VIEW' in req.perm: yield ('mainnav', 'browser', tag.a(_('Browse Source'), href=req.href.browser())) # IPermissionRequestor methods def get_permission_actions(self): return ['BROWSER_VIEW', 'FILE_VIEW'] # IRequestHandler methods def match_request(self, req): import re match = re.match(r'/(export|browser|file)(?:(/.*))?', req.path_info) if match: mode, path = match.groups() if mode == 'export': if path and '/' in path: _, rev, path = path.split('/', 2) req.args['rev'] = rev req.args['format'] = 'raw' elif mode == 'file': req.redirect(req.href.browser(path, rev=req.args.get('rev'), format=req.args.get('format')), permanent=True) req.args['path'] = path or '/' return True def process_request(self, req): go_to_preselected = req.args.get('preselected') if go_to_preselected: req.redirect(go_to_preselected) path = req.args.get('path', '/') rev = req.args.get('rev', None) order = req.args.get('order', None) desc = req.args.get('desc', None) xhr = req.get_header('X-Requested-With') == 'XMLHttpRequest' # Find node for the requested path/rev repos = self.env.get_repository(req.authname) try: if rev: rev = repos.normalize_rev(rev) # If `rev` is `None`, we'll try to reuse `None` consistently, # as a special shortcut to the latest revision. rev_or_latest = rev or repos.youngest_rev node = get_existing_node(req, repos, path, rev_or_latest) except NoSuchChangeset, e: raise ResourceNotFound(e.message, _('Invalid Changeset Number')) context = Context.from_request(req, 'source', path, node.created_rev) path_links = get_path_links(req.href, path, rev, order, desc) if len(path_links) > 1: add_link(req, 'up', path_links[-2]['href'], _('Parent directory')) data = { 'context': context, 'path': path, 'rev': node.rev, 'stickyrev': rev, 'created_path': node.created_path, 'created_rev': node.created_rev, 'properties': xhr or self.render_properties('browser', context, node.get_properties()),
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -