| 1 | from trac.core import * |
|---|
| 2 | from trac.wiki.macros import WikiMacroBase |
|---|
| 3 | from trac.mimeview.api import IHTMLPreviewAnnotator, Mimeview |
|---|
| 4 | from trac.wiki.api import parse_args |
|---|
| 5 | from genshi.builder import tag |
|---|
| 6 | from genshi.filters import Transformer |
|---|
| 7 | |
|---|
| 8 | try: |
|---|
| 9 | from trac.versioncontrol.api import IRepositoryProvider |
|---|
| 10 | multirepos = True |
|---|
| 11 | except: |
|---|
| 12 | multirepos = False |
|---|
| 13 | |
|---|
| 14 | class IncludeSourceMacro(WikiMacroBase): |
|---|
| 15 | """Includes a source file from the repository into the Wiki. |
|---|
| 16 | |
|---|
| 17 | There is one required parameter, which is the path to the |
|---|
| 18 | file to include. This should be the repository path, not a |
|---|
| 19 | full URL. |
|---|
| 20 | |
|---|
| 21 | Optional named parameters are: |
|---|
| 22 | * ''start '' The first line of the file to include. Defaults to the |
|---|
| 23 | beginning of the file. Otherwise should be a numeric value. |
|---|
| 24 | |
|---|
| 25 | Note that files start with line 1, not line 0. |
|---|
| 26 | * ''end'' The last line of the file to include. Defaults to the end |
|---|
| 27 | of the file. |
|---|
| 28 | |
|---|
| 29 | Note that both 'start' and 'end' are used for array slicing the |
|---|
| 30 | lines of the file, so if (for example) you want the last 20 lines |
|---|
| 31 | of the file, you can use start=-20 and leave end blank. |
|---|
| 32 | * ''rev'' Which revision to include. This defaults to HEAD if |
|---|
| 33 | not supplied. Otherwise this should be a valid numeric revision |
|---|
| 34 | number in your version control repository. |
|---|
| 35 | * ''mimetype'' Which mimetype to use to determine syntax highlighting. |
|---|
| 36 | If not supplied, this is determined by the file extension (which |
|---|
| 37 | is normally what you want) |
|---|
| 38 | |
|---|
| 39 | Examples: |
|---|
| 40 | {{{ |
|---|
| 41 | # include entire file |
|---|
| 42 | [[IncludeSource(trunk/proj/file.py)]] |
|---|
| 43 | |
|---|
| 44 | # includes line 20-50 inclusive |
|---|
| 45 | [[IncludeSource(trunk/proj/file.py, start=20, end=50)]] |
|---|
| 46 | |
|---|
| 47 | # includes last 30 lines of file at revision 1200 |
|---|
| 48 | [[IncludeSource(trunk/proj/file.py, start=-30, rev=1200)]] |
|---|
| 49 | |
|---|
| 50 | # include entire file but formatted plain |
|---|
| 51 | [[IncludeSource(trunk/proj/file.py, mimetype=text/plain)]] |
|---|
| 52 | |
|---|
| 53 | # includes line 20-50 inclusive and overrides file name link |
|---|
| 54 | # in header text |
|---|
| 55 | [[IncludeSource(trunk/proj/file.py, start=20, end=50, header=New header text)]] |
|---|
| 56 | |
|---|
| 57 | # includes line 20-50 inclusive and overrides file name link |
|---|
| 58 | # in header text, along with a specific CSS class (class must exist |
|---|
| 59 | # in CSS on page; there is no provision for defining it in this macro) |
|---|
| 60 | [[IncludeSource(trunk/proj/file.py, start=20, end=50, header=New header text, header_class=my_class)]] |
|---|
| 61 | |
|---|
| 62 | # includes line 20-50 inclusive, but suppresses the display of line numbers. |
|---|
| 63 | # (0, no, false, and none are all honored for suppressing - case insensitive) |
|---|
| 64 | [[IncludeSource(trunk/proj/file.py, start=20, end=50, line_numbers=0)]] |
|---|
| 65 | |
|---|
| 66 | }}} |
|---|
| 67 | |
|---|
| 68 | See TracLinks, TracSyntaxColoring and trac/mimeview/api.py |
|---|
| 69 | |
|---|
| 70 | TODO |
|---|
| 71 | {{{ |
|---|
| 72 | * Fix non-localized strings |
|---|
| 73 | |
|---|
| 74 | * Fix proper encoding of output |
|---|
| 75 | |
|---|
| 76 | * Implement some sort of caching (especially in cases where the |
|---|
| 77 | revision is known and we know that the contents won't change). |
|---|
| 78 | |
|---|
| 79 | * Allow multiple chunks from the file in one call. You can do this |
|---|
| 80 | with the existing code, but it will pull the entire file out of |
|---|
| 81 | version control and trim it for each chunk, so this could be |
|---|
| 82 | optimized a bit. This could be done with the Ranges object |
|---|
| 83 | |
|---|
| 84 | * Refactor code a bit - there are enough special cases in it now |
|---|
| 85 | that the expand_macro call is getting a bit unwieldy. |
|---|
| 86 | |
|---|
| 87 | }}} |
|---|
| 88 | """ |
|---|
| 89 | |
|---|
| 90 | def expand_macro(self, formatter, name, content): |
|---|
| 91 | self.log.info('Begin expand_macro for req: ' + repr(content)) |
|---|
| 92 | largs, kwargs = parse_args(content) |
|---|
| 93 | href = formatter.href |
|---|
| 94 | |
|---|
| 95 | if len(largs) == 0: |
|---|
| 96 | raise TracError("File name to include is required parameter!") |
|---|
| 97 | |
|---|
| 98 | orig_file_name = file_name = largs[0] |
|---|
| 99 | |
|---|
| 100 | global multirepos |
|---|
| 101 | if not multirepos: |
|---|
| 102 | repos = self.env.get_repository(formatter.req.authname) |
|---|
| 103 | else: |
|---|
| 104 | if (orig_file_name[0] == '/'): orig_file_name = orig_file_name[1:] |
|---|
| 105 | splitpath = file_name.split('/') |
|---|
| 106 | if (file_name[0] == '/'): |
|---|
| 107 | reponame = splitpath[1] |
|---|
| 108 | else: |
|---|
| 109 | reponame = splitpath[0] |
|---|
| 110 | repos = self.env.get_repository(reponame) |
|---|
| 111 | if (repos): |
|---|
| 112 | l = len(reponame) |
|---|
| 113 | if (file_name[0] == '/'): |
|---|
| 114 | file_name = file_name[1:] |
|---|
| 115 | file_name = file_name[l:] |
|---|
| 116 | else: |
|---|
| 117 | repos = self.env.get_repository() |
|---|
| 118 | |
|---|
| 119 | rev = kwargs.get('rev', None) |
|---|
| 120 | |
|---|
| 121 | if kwargs.has_key('header'): |
|---|
| 122 | header = kwargs.get('header') # user specified header |
|---|
| 123 | else: |
|---|
| 124 | header = tag.a(file_name, href=href.browser(orig_file_name, rev=rev)) |
|---|
| 125 | if not header: |
|---|
| 126 | header = u'\xa0' # default value from trac.mimeview.api.py |
|---|
| 127 | |
|---|
| 128 | # TODO - 'content' is default from mimeview.api.py, but it picks |
|---|
| 129 | # up text-align: center, which probably isn't the best thing if |
|---|
| 130 | # we are adding a file name in the header. There isn't an obvious |
|---|
| 131 | # replacement in the delivered CSS to pick over this for now though |
|---|
| 132 | header_class = kwargs.get('header_class', 'content') |
|---|
| 133 | |
|---|
| 134 | src = repos.get_node(file_name, rev).get_content().read() |
|---|
| 135 | |
|---|
| 136 | context = formatter.context |
|---|
| 137 | # put these into context object so annotator sees them |
|---|
| 138 | context.file_name = file_name |
|---|
| 139 | context.rev = rev |
|---|
| 140 | context.startline = 1 |
|---|
| 141 | |
|---|
| 142 | # we generally include line numbers in the output, unless it has been |
|---|
| 143 | # explicitly requested otherwise. 0, no, false, none will suppress |
|---|
| 144 | line_numbers = kwargs.get('line_numbers', None) |
|---|
| 145 | if line_numbers is None: |
|---|
| 146 | line_numbers = True |
|---|
| 147 | else: |
|---|
| 148 | try: |
|---|
| 149 | line_numbers = int(line_numbers) |
|---|
| 150 | except: |
|---|
| 151 | negatory = ('no', 'false', 'none') |
|---|
| 152 | line_numbers = str(line_numbers).lower() not in negatory |
|---|
| 153 | |
|---|
| 154 | # lines added up front to "trick" renderer when rendering partial |
|---|
| 155 | render_prepend = [] |
|---|
| 156 | |
|---|
| 157 | start, end = kwargs.get('start', None), kwargs.get('end', None) |
|---|
| 158 | if start or end: |
|---|
| 159 | src, start, end = self._handle_partial(src, start, end) |
|---|
| 160 | context.startline = start |
|---|
| 161 | |
|---|
| 162 | if start > 2 and file_name.endswith('.php'): |
|---|
| 163 | render_prepend = [ '#!/usr/bin/php -f', '<?' ] |
|---|
| 164 | |
|---|
| 165 | if render_prepend: |
|---|
| 166 | src = '\n'.join(render_prepend) + '\n' + src |
|---|
| 167 | |
|---|
| 168 | # ensure accurate start number after this gets stripped |
|---|
| 169 | context.startline = start - len(render_prepend) |
|---|
| 170 | |
|---|
| 171 | mimetype = kwargs.get('mimetype', None) |
|---|
| 172 | url = None # render method doesn't seem to use this |
|---|
| 173 | |
|---|
| 174 | mv = Mimeview(self.env) |
|---|
| 175 | annotations = line_numbers and ['givenlineno'] or None |
|---|
| 176 | |
|---|
| 177 | src = mv.render(formatter.context, mimetype, src, file_name, url, annotations) |
|---|
| 178 | |
|---|
| 179 | if line_numbers: |
|---|
| 180 | # handle the case where only one line of code was included |
|---|
| 181 | # and we get back an XHTML string |
|---|
| 182 | if not hasattr(src, 'generate'): |
|---|
| 183 | from genshi.input import XML |
|---|
| 184 | src = XML(src) |
|---|
| 185 | |
|---|
| 186 | # the _render_source method will always set the CSS class |
|---|
| 187 | # of the annotator to it's name; there isn't an easy way |
|---|
| 188 | # to override that. We could create our own CSS class for |
|---|
| 189 | # givenlineno that mimics lineno, but it's cleaner to just |
|---|
| 190 | # tweak the output here by running the genshi stream from |
|---|
| 191 | # src through a transformer that will make the change |
|---|
| 192 | |
|---|
| 193 | xpath1 = 'thead/tr/th[@class="givenlineno"]' |
|---|
| 194 | xpath2 = 'thead/tr/th[2]' # last() not supported by Genshi? |
|---|
| 195 | xpath3 = 'thead/tr/th[2]/text()' |
|---|
| 196 | |
|---|
| 197 | # TODO - does genshi require a QName here? Seems to work w/o it |
|---|
| 198 | src = src.generate() | Transformer(xpath1).attr('class', 'lineno') \ |
|---|
| 199 | | Transformer(xpath2).attr('class', header_class) \ |
|---|
| 200 | | Transformer(xpath3).replace(header) |
|---|
| 201 | |
|---|
| 202 | if render_prepend: |
|---|
| 203 | # TODO - is there a better of stripping lines here? |
|---|
| 204 | for i in xrange(len(render_prepend)): |
|---|
| 205 | src = src | Transformer('tbody/tr[1]').remove() |
|---|
| 206 | |
|---|
| 207 | return src |
|---|
| 208 | |
|---|
| 209 | def _handle_partial(self, src, start, end): |
|---|
| 210 | # we want to only show a certain number of lines, so we |
|---|
| 211 | # break the source into lines and set our numbers for 1-based |
|---|
| 212 | # line numbering. |
|---|
| 213 | # |
|---|
| 214 | # Note that there are some good performance enhancements that |
|---|
| 215 | # could be done by |
|---|
| 216 | # a) reading lines out of Subversion, using svn_stream_readline |
|---|
| 217 | # instead of svn_stream_read when fetching data |
|---|
| 218 | # b) have the render method accept a list/iterator of lines |
|---|
| 219 | # instead of only accepting a string (which it then splits) |
|---|
| 220 | lines = src.split('\n') |
|---|
| 221 | linecount = len(lines) |
|---|
| 222 | |
|---|
| 223 | if start: |
|---|
| 224 | start = int(start) |
|---|
| 225 | if start >= 0: |
|---|
| 226 | start -= 1 |
|---|
| 227 | if end: |
|---|
| 228 | end = int(end) |
|---|
| 229 | src = lines[start:end] |
|---|
| 230 | |
|---|
| 231 | # calculate actual startline for display purposes |
|---|
| 232 | if not start: |
|---|
| 233 | start = 1 |
|---|
| 234 | elif start < 0: |
|---|
| 235 | start = linecount + start + 1 |
|---|
| 236 | else: |
|---|
| 237 | start += 1 |
|---|
| 238 | |
|---|
| 239 | return '\n'.join(src), start, end |
|---|
| 240 | |
|---|
| 241 | class GivenLineNumberAnnotator(Component): |
|---|
| 242 | """Text annotator that adds a column with given line numbers.""" |
|---|
| 243 | implements(IHTMLPreviewAnnotator) |
|---|
| 244 | |
|---|
| 245 | # ITextAnnotator methods |
|---|
| 246 | |
|---|
| 247 | def get_annotation_type(self): |
|---|
| 248 | return 'givenlineno', 'Line', 'Line numbers' |
|---|
| 249 | |
|---|
| 250 | def get_annotation_data(self, context): |
|---|
| 251 | return None |
|---|
| 252 | |
|---|
| 253 | def annotate_row(self, context, row, lineno, line, data): |
|---|
| 254 | file_name = context.file_name |
|---|
| 255 | if file_name[0] == '/': |
|---|
| 256 | file_name = file_name[1:] |
|---|
| 257 | |
|---|
| 258 | rev = make_rev_str(context.rev) |
|---|
| 259 | |
|---|
| 260 | lineno = context.startline + lineno - 1 |
|---|
| 261 | |
|---|
| 262 | row.append(tag.th(id='L%s' % lineno)( |
|---|
| 263 | tag.a(lineno, href='../browser/%s%s#L%s' % (file_name, rev, lineno)) |
|---|
| 264 | )) |
|---|
| 265 | |
|---|
| 266 | def make_rev_str(rev=None): |
|---|
| 267 | return rev and '?rev=' + str(rev) or '' |
|---|