source: includesourcepartialplugin/1.2/includesource/IncludeSource.py

Last change on this file was 15931, checked in by Ryan J Ollos, 7 years ago

0.4: Pass rev as parameter

Fixes a regression in r15745.

Patch by ebouaziz@…

Fixes #6009.

File size: 9.9 KB
Line 
1from trac.core import *
2from trac.wiki.macros import WikiMacroBase
3from trac.mimeview.api import IHTMLPreviewAnnotator, Mimeview
4from trac.wiki.api import parse_args
5from genshi.builder import tag
6from genshi.filters import Transformer
7
8try:
9    from trac.versioncontrol.api import IRepositoryProvider
10    multirepos = True
11except:
12    multirepos = False
13
14class 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
241class 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
266def make_rev_str(rev=None):
267    return rev and '?rev=' + str(rev) or ''
Note: See TracBrowser for help on using the repository browser.