Edgewall Software

MacroBazaar: IncludeSource.py

File IncludeSource.py, 9.3 KB (added by chris.heller, 4 years ago)

Modified version of IncludeSource?. Supports partial includes, line numbering, auto-selection of formatter (with optional override)

Line 
1from trac.core import *
2from trac.wiki.macros import WikiMacroBase
3from trac.wiki.formatter import wiki_to_html
4from trac.mimeview.api import IHTMLPreviewAnnotator, Mimeview
5from os import linesep
6
7class IncludeSourceMacro(WikiMacroBase):
8    """Includes a source file from the repository into the Wiki.
9
10    There is one required parameter, which is the path to the
11    file to include. This should be the repository path, not a
12    full URL.
13
14    Optional named parameters are:
15     * ''start '' The first line of the file to include. Defaults to the
16     beginning of the file. Otherwise should be a numeric value.
17
18     Note that files start with line 1, not line 0.
19     * ''end'' The last line of the file to include. Defaults to the end
20     of the file.
21
22     Note that both 'start' and 'end' are used for array slicing the
23     lines of the file, so if (for example) you want the last 20 lines
24     of the file, you can use start=-20 and leave end blank.
25     * ''rev'' Which revision to include. This defaults to HEAD if
26     not supplied. Otherwise this should be a valid numeric revision
27     number in your version control repository.
28     * ''mimetype'' Which mimetype to use to determine syntax highlighting.
29     If not supplied, this is determined by the file extension (which
30     is normally what you want)
31
32    Examples:
33    {{{
34        # include entire file
35        [[IncludeSource(trunk/proj/file.py)]]
36
37        # includes line 20-50 inclusive
38        [[IncludeSource(trunk/proj/file.py, start=20, end=50)]]
39
40        # includes last 30 lines of file at revision 1200
41        [[IncludeSource(trunk/proj/file.py, start=-30, rev=1200)]]
42
43        # includes last 30 lines of file at revision 1200
44        [[IncludeSource(trunk/proj/file.py, start=-30, rev=1200)]]
45
46        # include entire file but formatted plain
47        [[IncludeSource(trunk/proj/file.py, mimetype=text/plain)]]
48
49    }}}
50
51    See TracLinks, TracSyntaxColoring and trac/mimeview/api.py
52
53    TODO
54    {{{
55    * Fix non-localized strings
56
57    * Fix proper encoding of output
58
59    * Implement some sort of caching (especially in cases where the
60    revision is known and we know that the contents won't change).
61
62    * Fix issue with calling IncludeSource more than once on a single
63    page. This causes some error in the C bindings for Subversion
64    so it will cause a server error instead of producing a log message.
65
66    See the comments in IncludeSource.py for more information about
67    where this occurs.
68
69    * Allow multiple chunks from the file in one call. You can do this
70    with the existing code, but it will pull the entire file out of
71    version control and trim it for each chunk, so this could be
72    optimized a bit.
73    }}}
74    """   
75
76    def render_macro(self, req, name, args):
77        self.log.warning('Begin render_macro for req: ' + repr(args))
78
79        largs, kwargs = parse_args(args)
80        repos = self.env.get_repository()
81       
82        self.log.warning('have repo')
83        if len(largs) == 0:
84            # TODO - don't hardcode this in English
85            raise Exception("File name to include is required parameter!")
86
87        try:
88            file_name = largs[0]
89           
90            rev = kwargs.get('rev', None)
91
92            # This dies in the C code when we have two instances of the
93            # macro on one request.  It dies in trac.versioncontrol.svn_fs
94            # in the SubversionNode __init__ method on the line
95            # self.root = fs.revision_root(self.fs_ptr, rev, self.pool())
96            #
97            # This could be from a bad fs_ptr or bad pool, but it wasn't
98            # obvious during some initial python level debugging as to what
99            # the cause is
100            src = repos.get_node(file_name, rev).get_content().read()
101
102            # We'd like to just use Mimeview directly here, but there's no
103            # support for the partial output that we want to support here
104            mv = IncludeSourceMimeview(self.env)
105            mv.file_name = file_name
106            mv.rev = rev
107
108            start, end = kwargs.get('start', None), kwargs.get('end', None)
109            if start or end:
110                src, start, end = self._handle_partial(src, start, end)
111                mv.startline = start
112
113            mimetype = kwargs.get('mimetype', None)
114            url = None  # render method doesn't seem to use this
115            src = mv.render(req, mimetype, src, file_name, url, ['givenlineno'])
116
117        finally:
118            repos.close()
119            repos = None
120        return src
121
122    def _handle_partial(self, src, start, end):
123        # we want to only show a certain number of lines, so we
124        # break the source into lines and set our numbers for 1-based
125        # line numbering.
126        #
127        # Note that there are some good performance enhancements that
128        # could be done by
129        # a) reading lines out of Subversion, using svn_stream_readline
130        #    instead of svn_stream_read when fetching data
131        # b) have the render method accept a list/iterator of lines
132        #    instead of only accepting a string (which it then splits)
133        lines = src.split(linesep)
134        linecount = len(lines)
135
136        if start:
137            start = int(start)
138            if start >= 0:
139                start -= 1
140        if end:
141            end = int(end)
142        src = lines[start:end]
143
144        # calculate actual startline for display purposes
145        if not start:
146            start = 1
147        elif start < 0:
148            start = linecount + start + 1
149        else:
150            start += 1
151
152        return linesep.join(src), start, end
153
154class GivenLineNumberAnnotator(Component):
155    """Text annotator that adds a column with given line numbers."""
156    implements(IHTMLPreviewAnnotator)
157
158    file_name = ''
159    rev = None
160
161    # ITextAnnotator methods
162
163    def get_annotation_type(self):
164        return 'givenlineno', 'Line', 'Line numbers'
165
166    def annotate_line(self, number, content):
167        if self.file_name.startswith('/'):
168            self.file_name = self.file_name[1:]
169
170        rev = self.rev and '@' + str(self.rev) or ''
171
172        return '<th id="L%s"><a href="../browser/%s%s#L%s">%s</a></th>' % \
173            (number, self.file_name, rev, number, number)
174
175class IncludeSourceMimeview(Mimeview):
176
177    startline = 1
178    file_name = ''
179    rev = None
180
181    def _annotate(self, lines, annotations):
182        import re
183        from StringIO import StringIO
184        from trac.mimeview.api import _html_splitlines
185
186        buf = StringIO()
187        buf.write('<table class="code"><thead><tr>')
188        annotators = []
189        for annotator in self.annotators:
190            atype, alabel, adesc = annotator.get_annotation_type()
191            if atype in annotations:
192
193                # kludge to get lineno CSS class
194                if atype == 'givenlineno':
195                    atype = 'lineno'   
196
197                buf.write('<th class="%s">%s</th>' % (atype, alabel))
198                annotators.append(annotator)
199        buf.write('<th class="content">&nbsp; </th>')
200        buf.write('</tr></thead><tbody>')
201
202        space_re = re.compile('(?P<spaces> (?: +))|'
203                              '^(?P<tag><\w+.*?>)?( )')
204        def htmlify(match):
205            m = match.group('spaces')
206            if m:
207                div, mod = divmod(len(m), 2)
208                return div * '&nbsp; ' + mod * '&nbsp;'
209            return (match.group('tag') or '') + '&nbsp;'
210
211        num = -1
212        for num, line in enumerate(_html_splitlines(lines)):
213            cells = []
214            linenum = num + self.startline
215            for annotator in annotators:
216
217                # kludgey way of passing data to annotator
218                if annotator.get_annotation_type()[0] == 'givenlineno':
219                    annotator.file_name = self.file_name
220                    annotator.rev = self.rev
221
222                cells.append(annotator.annotate_line(linenum, line))
223            cells.append('<td>%s</td>\n' % space_re.sub(htmlify, line))
224            buf.write('<tr>' + '\n'.join(cells) + '</tr>')
225        else:
226            if num < 0:
227                return ''
228        buf.write('</tbody></table>')
229        return buf.getvalue()
230
231
232
233# see ticket 2983 - this parse_args is added to Trac there
234# so we'll define it here if someone is on a version without that
235# http://trac.edgewall.org/ticket/2983
236try:
237    from trac.wiki.api import parse_args
238except:
239    import re
240    def parse_args(args):
241        """Utility for parsing macro "content" and splitting them into arguments.
242
243        The content is split along commas, unless they are escaped with a
244        backquote (like this: \,).
245        Named arguments a la Python are supported, and keys must be  valid python
246        identifiers immediately followed by the "=" sign.
247
248        >>> parse_args('')
249        ([], {})
250        >>> parse_args('Some text')
251        (['Some text'], {})
252        >>> parse_args('Some text, mode= 3, some other arg\, with a comma.')
253        (['Some text', ' some other arg, with a comma.'], {'mode': ' 3'})
254       
255        """   
256        largs, kwargs = [], {}
257        if args:
258            for arg in re.split(r'(?<!\\),', args):
259                arg = arg.replace(r'\,', ',')
260                m = re.match(r'\s*[a-zA-Z_]\w+=', arg)
261                if m:
262                    kwargs[arg[:m.end()-1].lstrip()] = arg[m.end():]
263                else:
264                    largs.append(arg)
265        return largs, kwargs
266
267