source: doxygenplugin/0.11/doxygentrac/doxygentrac.py

Last change on this file was 12233, checked in by Ryan J Ollos, 11 years ago

Fixes #7247: Changed the default value for html_output to be an empty string.

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