source: doxygenplugin/0.10/doxygentrac/doxygentrac.py

Last change on this file was 3375, checked in by Christian Boos, 16 years ago

Applied patch from xl948 for fixing #798 and #T5574.

Thanks!

File size: 16.8 KB
RevLine 
[1209]1# vim: ts=4 expandtab
2#
3# Copyright (C) 2005 Jason Parks <jparks@jparks.net>. All rights reserved.
4#
5
6from __future__ import generators
7
8import os
9import time
10import posixpath
11import re
12import mimetypes
13
14from trac.config import Option
15from trac.core import *
16from trac.web import IRequestHandler
17from trac.perm import IPermissionRequestor
18from trac.web.chrome import INavigationContributor, ITemplateProvider, \
19  add_stylesheet
20from trac.Search import ISearchSource
[1226]21from trac.wiki import WikiSystem, IWikiSyntaxProvider
22from trac.wiki.model import WikiPage
[1251]23from trac.wiki.formatter import wiki_to_html
[1209]24from trac.util.html import html
25
26def compare_rank(x, y):
27    if x['rank'] == y['rank']:
28        return 0
29    elif x['rank'] > y['rank']:
30        return -1
31    return 1
32
33class DoxygenPlugin(Component):
34    implements(IPermissionRequestor, INavigationContributor, IRequestHandler,
35      ITemplateProvider, ISearchSource, IWikiSyntaxProvider)
36
37    base_path = Option('doxygen', 'path', '/var/lib/trac/doxygen',
38      """Directory containing doxygen generated files.""")
39
40    default_doc = Option('doxygen', 'default_documentation', '',
[1245]41      """Default documentation project, relative to `[doxygen] path`.
42      When no explicit path is given in a documentation request,
43      this path will be prepended to the request before looking
44      for documentation files.""")
[1209]45
[1249]46    html_output = Option('doxygen', 'html_output', None,
47      """Default documentation project suffix, as generated by Doxygen
48      using the HTML_OUTPUT Doxygen configuration setting.""")
49
[1209]50    title = Option('doxygen', 'title', 'Doxygen',
51      """Title to use for the main navigation tab.""")
52
53    ext = Option('doxygen', 'ext', 'htm html png',
54      """Space separated list of extensions for doxygen managed files.""")
55
56    source_ext = Option('doxygen', 'source_ext',
57      'idl odl java cs py php php4 inc phtml m '
58      'cpp cxx c hpp hxx h',
59      """Space separated list of source files extensions""")
60
61    index = Option('doxygen', 'index', 'main.html',
62      """Default index page to pick in the generated documentation.""")
63
64    wiki_index = Option('doxygen', 'wiki_index', None,
[1245]65      """Wiki page to use as the default page for the Doxygen main page.
66      If set, supersedes the `[doxygen] index` option.""")
[1209]67
68    encoding = Option('doxygen', 'encoding', 'iso-8859-1',
69      """Default encoding used by the generated documentation files.""")
70
[1226]71    SUMMARY_PAGES = """
72    annotated classes dirs files functions globals hierarchy
73    index inherits main namespaces namespacemembers
74    """.split()
75
[1209]76    # IPermissionRequestor methods
77
78    def get_permission_actions(self):
79        return ['DOXYGEN_VIEW']
80
81    # INavigationContributor methods
82
83    def get_active_navigation_item(self, req):
84        return 'doxygen'
85
86    def get_navigation_items(self, req):
87        if req.perm.has_permission('DOXYGEN_VIEW'):
88            # Return mainnav buttons.
[1244]89            yield 'mainnav', 'doxygen', html.a(self.title,
90                                               href=req.href.doxygen())
[1209]91
92    # IRequestHandler methods
93
94    def match_request(self, req):
[1226]95        if re.match(r'^/doxygen(?:$|/)', req.path_info):
[1249]96            if 'path' not in req.args: # not coming from a `doxygen:` link
97                segments = filter(None, req.path_info.split('/'))
98                segments = segments[1:] # ditch 'doxygen'
99                if segments:
100                    action, path, link = self._doxygen_lookup(segments)
101                    if action == 'search' and link:
102                        req.args['query'] = link
103                    elif action == 'redirect':
104                        req.args['link'] = link
105                else:
106                    action, path = 'index', ''
107                req.args['action'] = action
108                req.args['path'] = path
[1209]109            return True
[2651]110
[1209]111    def process_request(self, req):
112        req.perm.assert_permission('DOXYGEN_VIEW')
113
114        # Get request arguments
115        path = req.args.get('path')
116        action = req.args.get('action')
[1244]117        link = req.args.get('link')
[1209]118
[1244]119        self.log.debug('Performing %s(%s,%s)"' % (action or 'default',
120                                                  path, link))
[1209]121
122        # Redirect search requests.
123        if action == 'search':
[1226]124            req.redirect(req.href.search(q=req.args.get('query'),
125                                         doxygen='on'))
[1244]126        if action == 'redirect':
127            if link: # we need to really redirect if there is a link
128                if path:
129                    req.redirect(req.href.doxygen(path=path)+link)
130                else:
131                    req.redirect(req.href.doxygen(link))
132            else:
133                self.log.warn("redirect without link")
[1209]134
[1983]135        if req.path_info == '/doxygen':
136            req.redirect(req.href.doxygen('/'))
137
[1226]138        # Handle /doxygen request
139        if action == 'index':
[1251]140            wiki = self.wiki_index
141            if wiki:
142                if WikiSystem(self.env).has_page(wiki):
143                    text = WikiPage(self.env, wiki).text
144                else:
145                    text = 'Doxygen index page [wiki:%s] does not exist.' % \
146                           wiki
147                text = wiki_to_html(text, self.env, req)
148                req.hdf['doxygen.text'] = text
149                req.hdf['doxygen.wiki_href'] = req.href.wiki(wiki)
150                req.hdf['doxygen.wiki_page'] = wiki
[1209]151                return 'doxygen.cs', 'text/html'
[2651]152
[1251]153            # use configured Doxygen index
154            path = os.path.join(self.base_path, self.default_doc,
155                                self.html_output, self.index)
[2651]156
157        self.log.debug('path: %s' % (path,))
158
[1983]159        # security check
160        path = os.path.abspath(path)
161        if not path.startswith(self.base_path):
162            raise TracError("Can't access paths outside of " + self.base_path)
[1209]163
[1983]164        # view
[1226]165        mimetype = mimetypes.guess_type(path)[0]
166        if mimetype == 'text/html':
167            add_stylesheet(req, 'doxygen/css/doxygen.css')
168            req.hdf['doxygen.path'] = path
169            return 'doxygen.cs', 'text/html'
170        else:
[2651]171            req.send_file(path, mimetype)
[1209]172
173    # ITemplateProvider methods
174
175    def get_htdocs_dirs(self):
176        from pkg_resources import resource_filename
177        return [('doxygen', resource_filename(__name__, 'htdocs'))]
178
179    def get_templates_dirs(self):
180        from pkg_resources import resource_filename
181        return [resource_filename(__name__, 'templates')]
182
183    # ISearchProvider methods
184
185    def get_search_filters(self, req):
186        if req.perm.has_permission('DOXYGEN_VIEW'):
187            yield('doxygen', self.title)
188
189    def get_search_results(self, req, keywords, filters):
190        if not 'doxygen' in filters:
191            return
192
193        # We have to search for the raw bytes...
194        keywords = [k.encode(self.encoding) for k in keywords]
195
196        for doc in os.listdir(self.base_path):
197            # Search in documentation directories
198            path = os.path.join(self.base_path, doc)
[3375]199            path = os.path.join(path, self.html_output)
[1209]200            if os.path.isdir(path):
201                index = os.path.join(path, 'search.idx')
202                if os.path.exists(index):
203                    creation = os.path.getctime(index)
204                    for result in  self._search_in_documentation(doc, keywords):
205                        result['url'] =  req.href.doxygen(doc) + '/' \
206                          + result['url']
207                        yield result['url'], result['name'], creation, \
208                          'doxygen', None
209
210            # Search in common documentation directory
[3375]211            index = os.path.join(self.base_path, self.html_output)
212            index = os.path.join(index, 'search.idx')
[1209]213            if os.path.exists(index):
214                creation = os.path.getctime(index)
215                for result in self._search_in_documentation('', keywords):
216                    result['url'] =  req.href.doxygen() + '/' + \
217                      result['url']
218                    yield result['url'], result['name'], creation, 'doxygen', \
219                      None
220
221    # IWikiSyntaxProvider
[2651]222
[1209]223    def get_link_resolvers(self):
[1226]224        def doxygen_link(formatter, ns, params, label):
[1249]225            if '/' not in params:
226                params = self.default_doc+'/'+params
227            segments = params.split('/')
228            if self.html_output:
229                segments[-1:-1] = [self.html_output]
230            action, path, link = self._doxygen_lookup(segments)
[1244]231            if action == 'index':
232                return html.a(label, title=self.title,
233                              href=formatter.href.doxygen())
[1249]234            if action == 'redirect' and path:
235                return html.a(label, title="Search result for "+params,
236                              href=formatter.href.doxygen(link,path=path))
237            if action == 'search':
238                return html.a(label, title=params, class_='missing',
239                              href=formatter.href.doxygen())
240            else:
[1226]241                return html.a(label, title=params,
242                              href=formatter.href.doxygen(link, path=path))
243        yield ('doxygen', doxygen_link)
[1209]244
245    def get_wiki_syntax(self):
246        return []
247
248    # internal methods
[1226]249
250    def _doxygen_lookup(self, segments):
251        """Try to interpret path components as a request for doxygen targets
252
[1249]253        Return an `(action,path,link)` tuple, where:
[1226]254         - `action` describes what should be done (one of 'view',
[1249]255           'redirect', or 'search'),
[1226]256         - `path` is the location on disk of the resource.
257         - `link` is the link to the resource, relative to the
[1244]258           req.href.doxygen base or a target in case of 'redirect'
[1226]259        """
260        doc, file = segments[:-1], segments and segments[-1]
[1244]261
262        if not doc and not file:
263            return ('index', None, None) 
264        if doc:
265            doc = os.path.join(*doc)
266        else:
267            if self.default_doc: # we can't stay at the 'doxygen/' level
[1249]268                return 'redirect', None, '/'.join([self.default_doc,
269                                                   self.html_output,
270                                                   file or self.index])
[1244]271            else:
[2651]272                doc = self.html_output
273
[1226]274        def lookup(file, category='undefined'):
[1244]275            """Build (full path, relative link) and check if path exists."""
[1226]276            path = os.path.join(self.base_path, doc, file)
[1249]277            existing_path = os.path.exists(path) and path
278            link = doc+'/'+file
279            self.log.debug(' %s file %s' % (category, existing_path or
280                                            path+" (not found)"))
281            return existing_path, link
[1226]282
[1249]283        self.log.debug('Looking up "%s" in documentation "%s"' % (file, doc))
[1226]284
285        # Direct request for searching
286        if file == 'search.php':
[1244]287            return 'search', None, None # keep existing 'query' arg
[1226]288
289        # Request for a documentation file.
290        doc_ext_re = '|'.join(self.ext.split(' '))
291        if re.match(r'''^(.*)[.](%s)''' % doc_ext_re, file):
292            path, link = lookup(file, 'documentation')
293            if path:
294                return 'view', path, link
295            else:
[1244]296                return 'search', None, file
[1226]297
298        # Request for source file documentation.
299        source_ext_re = '|'.join(self.source_ext.split(' '))
300        match = re.match(r'''^(.*)[.](%s)''' % source_ext_re, file)
301        if match:
302            basename, suffix = match.groups()
303            basename = basename.replace('_', '__')
304            path, link = lookup('%s_8%s.html' % (basename, suffix), 'source')
305            if path:
306                return 'view', path, link
307            else:
[1244]308                return 'search', None, file
[1226]309
310        # Request for summary pages
311        if file in self.SUMMARY_PAGES:
312            path, link = lookup(file + '.html', 'summary')
313            if path:
314                return 'view', path, link
315
316        # Request for a named object
317        # TODO:
318        #  - do something about dirs
319        #  - expand with enum, defs, etc.
320        #  - this doesn't work well with the CREATE_SUBDIRS Doxygen option
321        path, link = lookup('class%s.html' % file, 'class')
322        if not path:
323            path, link = lookup('struct%s.html' % file, 'struct')
324        if path:
325            return 'view', path, link
326
327        # Revert to search...
328        results = self._search_in_documentation(doc, [file])
329        class_ref = file+' Class Reference'
330        for result in results:
331            self.log.debug('Reverted to search, found: ' + repr(result))
332            name = result['name']
333            if name == file or name == class_ref:
[1244]334                url = result['url']
335                target = ''
336                if '#' in url:
337                    url, target = url.split('#', 2)
338                path, link = lookup(url)
339                if path:
[1249]340                    return 'redirect', path, link # target # FIXME
[1226]341        self.log.debug('%s not found in %s' % (file, doc))
[1244]342        return 'search', None, file
[1226]343
[1209]344    def _search_in_documentation(self, doc, keywords):
345        # Open index file for documentation
[3375]346        index = os.path.join(self.base_path, doc, self.html_output, 'search.idx')
[1209]347        if os.path.exists(index):
348            fd = open(index)
349
350            # Search for keywords in index
351            results = []
352            for keyword in keywords:
353                results += self._search(fd, keyword)
354                results.sort(compare_rank)
355                for result in results:
356                    yield result
357
358    def _search(self, fd, word):
359        results = []
360        index = self._computeIndex(word)
361        if index != -1:
362            fd.seek(index * 4 + 4, 0)
363            index = self._readInt(fd)
364
365            if index:
366                fd.seek(index)
367                w = self._readString(fd)
368                matches = []
369                while w != "":
370                    statIdx = self._readInt(fd)
371                    low = word.lower()
372                    if w.find(low) != -1:
373                        matches.append({'word': word, 'match': w,
374                         'index': statIdx, 'full': len(low) == len(w)})
375                    w = self._readString(fd)
376
377                count = 0
378                totalHi = 0
379                totalFreqHi = 0
380                totalFreqLo = 0
381
382                for match in matches:
383                    multiplier = 1
384                    if match['full']:
385                        multiplier = 2
386
387                    fd.seek(match['index'])
388                    numDocs = self._readInt(fd)
389
390                    for i in range(numDocs):
391                        idx = self._readInt(fd)
[3375]392                        if idx == -1:
393                            freq = 0
394                        else:
395                            freq = self._readInt(fd)
[1209]396                        results.append({'idx': idx, 'freq': freq >> 1,
397                          'hi': freq & 1, 'multi': multiplier})
398                        if freq & 1:
399                            totalHi += 1
400                            totalFreqHi += freq * multiplier
401                        else:
402                            totalFreqLo += freq * multiplier
403
404                    for i in range(numDocs):
[3375]405                        if results[count]['idx'] == -1:
406                            results[count]['name'] = ''
407                            results[count]['url'] = ''
408                            count += 1
409                            continue
[1209]410                        fd.seek(results[count]['idx'])
411                        name = self._readString(fd)
412                        url = self._readString(fd)
413                        results[count]['name'] = name
[3375]414                        results[count]['url'] = self.html_output + '/' + url
[1209]415                        count += 1
416
417                totalFreq = (totalHi + 1) * totalFreqLo + totalFreqHi
418                for i in range(count):
419                    freq = results[i]['freq']
420                    multi = results[i]['multi']
421                    if results[i]['hi']:
422                        results[i]['rank'] = float(freq*multi + totalFreqLo) \
423                          / float(totalFreq)
424                    else:
425                        results[i]['rank'] = float(freq*multi) \
426                          / float(totalFreq)
427        return results
428
429    def _computeIndex(self, word):
430        if len(word) < 2:
431            return -1
432
433        hi = ord(word[0].lower())
434        if hi == 0:
435            return -1
436
437        lo = ord(word[1].lower())
438        if lo == 0:
439            return -1
440
441        return hi * 256 + lo
442
443    def _readInt(self, fd):
444        b1 = fd.read(1)
445        b2 = fd.read(1)
446        b3 = fd.read(1)
447        b4 = fd.read(1)
[3375]448       
449        if not b1 or not b2 or not b3 or not b4:
450            return -1;
[1209]451
452        return (ord(b1) << 24) | (ord(b2) << 16) | (ord(b3) << 8) | ord(b4)
453
454    def _readString(self, fd):
455        result = ''
456        byte = fd.read(1)
457        while byte != '\0':
458            result = ''.join([result, byte])
459            byte = fd.read(1)
460        return result
Note: See TracBrowser for help on using the repository browser.