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
RevLine 
[2069]1# -*- coding: utf-8 -*-
[1209]2# vim: ts=4 expandtab
3#
4# Copyright (C) 2005 Jason Parks <jparks@jparks.net>. All rights reserved.
[2069]5# Copyright (C) 2006-2007 Christian Boos <cboos@neuf.fr>
[1209]6#
7
8import os
9import time
10import posixpath
11import re
12import mimetypes
13
[2069]14from genshi.builder import tag
[3132]15from genshi.core import Markup
[2069]16
[1209]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, \
[3132]22                            add_stylesheet, add_ctxtnav
[2069]23from trac.search.api import ISearchSource
[3737]24from trac.util.text import to_unicode
[5596]25from trac.util.datefmt import to_datetime
[2069]26from trac.wiki.api import WikiSystem, IWikiSyntaxProvider
[1226]27from trac.wiki.model import WikiPage
[1251]28from trac.wiki.formatter import wiki_to_html
[1209]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,
[2069]39               ITemplateProvider, ISearchSource, IWikiSyntaxProvider)
[1209]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', '',
[1245]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.""")
[1209]49
[12233]50    html_output = Option('doxygen', 'html_output', '',
[1249]51      """Default documentation project suffix, as generated by Doxygen
52      using the HTML_OUTPUT Doxygen configuration setting.""")
53
[1209]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,
[1245]69      """Wiki page to use as the default page for the Doxygen main page.
70      If set, supersedes the `[doxygen] index` option.""")
[1209]71
72    encoding = Option('doxygen', 'encoding', 'iso-8859-1',
73      """Default encoding used by the generated documentation files.""")
74
[3735]75    default_namespace = Option('doxygen', 'default_namespace', '',
76      """Default namespace to search for named objects in.""")
77
[1226]78    SUMMARY_PAGES = """
79    annotated classes dirs files functions globals hierarchy
80    index inherits main namespaces namespacemembers
81    """.split()
82
[1209]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.
[2069]96            yield ('mainnav', 'doxygen',
97                   tag.a(self.title, href=req.href.doxygen()))
[1209]98
99    # IRequestHandler methods
100
101    def match_request(self, req):
[1226]102        if re.match(r'^/doxygen(?:$|/)', req.path_info):
[1249]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
[1209]116            return True
[2651]117
[1209]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')
[1244]124        link = req.args.get('link')
[1209]125
[1244]126        self.log.debug('Performing %s(%s,%s)"' % (action or 'default',
127                                                  path, link))
[1209]128
129        # Redirect search requests.
130        if action == 'search':
[1226]131            req.redirect(req.href.search(q=req.args.get('query'),
132                                         doxygen='on'))
[1244]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")
[1209]141
[1983]142        if req.path_info == '/doxygen':
143            req.redirect(req.href.doxygen('/'))
144
[1226]145        # Handle /doxygen request
146        if action == 'index':
[1251]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
[3132]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'
[1251]157            # use configured Doxygen index
158            path = os.path.join(self.base_path, self.default_doc,
159                                self.html_output, self.index)
[2651]160
161        self.log.debug('path: %s' % (path,))
162
[1983]163        # security check
164        path = os.path.abspath(path)
[6328]165        if not path.startswith(os.path.normpath(self.base_path)):
[1983]166            raise TracError("Can't access paths outside of " + self.base_path)
[1209]167
[1983]168        # view
[1226]169        mimetype = mimetypes.guess_type(path)[0]
170        if mimetype == 'text/html':
171            add_stylesheet(req, 'doxygen/css/doxygen.css')
[3132]172            # Genshi can't include an unparsed file
173            # data = {'doxygen_path': path}
174            try:
[3737]175                charset = (self.encoding or 
176                           self.env.config['trac'].get('default_charset'))
177                content = Markup(to_unicode(file(path).read(), charset))
[3132]178                data = {'doxygen_content': content}
179                return 'doxygen.html', data, 'text/html'
[6328]180            except (IOError, OSError), e:
[3132]181                raise TracError("Can't read doxygen content: %s" % e)
[1226]182        else:
183            req.send_file(path, mimetype)           
[1209]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):
[3735]202        self.log.debug("DOXYBUG: kw=%s f=%s" % (keywords, filters))
[1209]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)
[3550]212            path = os.path.join(path, self.html_output)
[3735]213            self.log.debug("looking in doc (%s) dir: %s:" % (doc, path))
[1209]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']
[5596]221                        yield result['url'], result['name'], to_datetime(creation), \
[1209]222                          'doxygen', None
223
224            # Search in common documentation directory
[3550]225            index = os.path.join(self.base_path, self.html_output)
226            index = os.path.join(index, 'search.idx')
[3735]227            self.log.debug("looking in doc (%s) search.idx: %s:" % (doc, index))
[1209]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']
[5596]233                    yield result['url'], result['name'], to_datetime(creation), 'doxygen', \
[1209]234                      None
235
236    # IWikiSyntaxProvider
[2651]237
[1209]238    def get_link_resolvers(self):
[1226]239        def doxygen_link(formatter, ns, params, label):
[1249]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)
[1244]246            if action == 'index':
[2069]247                return tag.a(label, title=self.title,
248                             href=formatter.href.doxygen())
[1249]249            if action == 'redirect' and path:
[2069]250                return tag.a(label, title="Search result for "+params,
251                             href=formatter.href.doxygen(link,path=path))
[1249]252            if action == 'search':
[2069]253                return tag.a(label, title=params, class_='missing',
254                             href=formatter.href.doxygen())
[1249]255            else:
[2069]256                return tag.a(label, title=params,
257                             href=formatter.href.doxygen(link, path=path))
[1226]258        yield ('doxygen', doxygen_link)
[1209]259
260    def get_wiki_syntax(self):
261        return []
262
263    # internal methods
[1226]264
265    def _doxygen_lookup(self, segments):
266        """Try to interpret path components as a request for doxygen targets
267
[1249]268        Return an `(action,path,link)` tuple, where:
[1226]269         - `action` describes what should be done (one of 'view',
[1249]270           'redirect', or 'search'),
[1226]271         - `path` is the location on disk of the resource.
272         - `link` is the link to the resource, relative to the
[1244]273           req.href.doxygen base or a target in case of 'redirect'
[1226]274        """
275        doc, file = segments[:-1], segments and segments[-1]
[1244]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
[1249]283                return 'redirect', None, '/'.join([self.default_doc,
284                                                   self.html_output,
285                                                   file or self.index])
[1244]286            else:
[2651]287                doc = self.html_output
288
[1226]289        def lookup(file, category='undefined'):
[1244]290            """Build (full path, relative link) and check if path exists."""
[1226]291            path = os.path.join(self.base_path, doc, file)
[1249]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
[1226]297
[1249]298        self.log.debug('Looking up "%s" in documentation "%s"' % (file, doc))
[1226]299
300        # Direct request for searching
301        if file == 'search.php':
[1244]302            return 'search', None, None # keep existing 'query' arg
[1226]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:
[1244]311                return 'search', None, file
[1226]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:
[1244]323                return 'search', None, file
[1226]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
[3735]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')
[1226]352        if not path:
[3735]353            path, link = lookup('struct%s.html' % mangledfile, 'struct')
[1226]354        if path:
355            return 'view', path, link
356
[3735]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
[1226]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:
[1244]374                url = result['url']
375                target = ''
376                if '#' in url:
377                    url, target = url.split('#', 2)
378                path, link = lookup(url)
379                if path:
[1249]380                    return 'redirect', path, link # target # FIXME
[1226]381        self.log.debug('%s not found in %s' % (file, doc))
[1244]382        return 'search', None, file
[1226]383
[1209]384    def _search_in_documentation(self, doc, keywords):
385        # Open index file for documentation
[3550]386        index = os.path.join(self.base_path, doc, self.html_output, 'search.idx')
[1209]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)
[3550]432                        if idx == -1:
433                            freq = 0
434                        else:
435                            freq = self._readInt(fd)
[1209]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):
[3550]445                        if results[count]['idx'] == -1:
446                            results[count]['name'] = ''
447                            results[count]['url'] = ''
448                            count += 1
449                            continue
[1209]450                        fd.seek(results[count]['idx'])
451                        name = self._readString(fd)
452                        url = self._readString(fd)
453                        results[count]['name'] = name
[3550]454                        results[count]['url'] = self.html_output + '/' + url
[1209]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)
[3550]488       
489        if not b1 or not b2 or not b3 or not b4:
490            return -1;
[1209]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.