Ticket #798: doxygentrac.py

File doxygentrac.py, 16.8 kB (added by xl948, 8 months ago)

Fix for bugs #798 and #5774

Line 
1 # vim: ts=4 expandtab
2 #
3 # Copyright (C) 2005 Jason Parks <jparks@jparks.net>. All rights reserved.
4 #
5
6 from __future__ import generators
7
8 import os
9 import time
10 import posixpath
11 import re
12 import mimetypes
13
14 from trac.config import Option
15 from trac.core import *
16 from trac.web import IRequestHandler
17 from trac.perm import IPermissionRequestor
18 from trac.web.chrome import INavigationContributor, ITemplateProvider, \
19   add_stylesheet
20 from trac.Search import ISearchSource
21 from trac.wiki import WikiSystem, IWikiSyntaxProvider
22 from trac.wiki.model import WikiPage
23 from trac.wiki.formatter import wiki_to_html
24 from trac.util.html import html
25
26 def 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
33 class 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', '',
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.""")
45
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
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,
65       """Wiki page to use as the default page for the Doxygen main page.
66       If set, supersedes the `[doxygen] index` option.""")
67
68     encoding = Option('doxygen', 'encoding', 'iso-8859-1',
69       """Default encoding used by the generated documentation files.""")
70
71     SUMMARY_PAGES = """
72     annotated classes dirs files functions globals hierarchy
73     index inherits main namespaces namespacemembers
74     """.split()
75
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.
89             yield 'mainnav', 'doxygen', html.a(self.title,
90                                                href=req.href.doxygen())
91
92     # IRequestHandler methods
93
94     def match_request(self, req):
95         if re.match(r'^/doxygen(?:$|/)', req.path_info):
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
109             return True
110
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')
117         link = req.args.get('link')
118
119         self.log.debug('Performing %s(%s,%s)"' % (action or 'default',
120                                                   path, link))
121
122         # Redirect search requests.
123         if action == 'search':
124             req.redirect(req.href.search(q=req.args.get('query'),
125                                          doxygen='on'))
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")
134
135         if req.path_info == '/doxygen':
136             req.redirect(req.href.doxygen('/'))
137
138         # Handle /doxygen request
139         if action == 'index':
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
151                 return 'doxygen.cs', 'text/html'
152
153             # use configured Doxygen index
154             path = os.path.join(self.base_path, self.default_doc,
155                                 self.html_output, self.index)
156
157         self.log.debug('path: %s' % (path,))
158
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)
163
164         # view
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:
171             req.send_file(path, mimetype)
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)
199             path = os.path.join(path, self.html_output)
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
211             index = os.path.join(self.base_path, self.html_output)
212             index = os.path.join(index, 'search.idx')
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
222
223     def get_link_resolvers(self):
224         def doxygen_link(formatter, ns, params, label):
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)
231             if action == 'index':
232                 return html.a(label, title=self.title,
233                               href=formatter.href.doxygen())
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:
241                 return html.a(label, title=params,
242                               href=formatter.href.doxygen(link, path=path))
243         yield ('doxygen', doxygen_link)
244
245     def get_wiki_syntax(self):
246         return []
247
248     # internal methods
249
250     def _doxygen_lookup(self, segments):
251         """Try to interpret path components as a request for doxygen targets
252
253         Return an `(action,path,link)` tuple, where:
254          - `action` describes what should be done (one of 'view',
255            'redirect', or 'search'),
256          - `path` is the location on disk of the resource.
257          - `link` is the link to the resource, relative to the
258            req.href.doxygen base or a target in case of 'redirect'
259         """
260         doc, file = segments[:-1], segments and segments[-1]
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
268                 return 'redirect', None, '/'.join([self.default_doc,
269                                                    self.html_output,
270                                                    file or self.index])
271             else:
272                 doc = self.html_output
273
274         def lookup(file, category='undefined'):
275             """Build (full path, relative link) and check if path exists."""
276             path = os.path.join(self.base_path, doc, file)
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
282
283         self.log.debug('Looking up "%s" in documentation "%s"' % (file, doc))
284
285         # Direct request for searching
286         if file == 'search.php':
287             return 'search', None, None # keep existing 'query' arg
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:
296                 return 'search', None, file
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:
308                 return 'search', None, file
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:
334                 url = result['url']
335                 target = ''
336                 if '#' in url:
337                     url, target = url.split('#', 2)
338                 path, link = lookup(url)
339                 if path:
340                     return 'redirect', path, link # target # FIXME
341         self.log.debug('%s not found in %s' % (file, doc))
342         return 'search', None, file
343
344     def _search_in_documentation(self, doc, keywords):
345         # Open index file for documentation
346         index = os.path.join(self.base_path, doc, self.html_output, 'search.idx')
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)
392                         if idx == -1:
393                             freq = 0
394                         else:
395                             freq = self._readInt(fd)
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):
405                         if results[count]['idx'] == -1:
406                             results[count]['name'] = ''
407                             results[count]['url'] = ''
408                             count += 1
409                             continue
410                         fd.seek(results[count]['idx'])
411                         name = self._readString(fd)
412                         url = self._readString(fd)
413                         results[count]['name'] = name
414                         results[count]['url'] = self.html_output + '/' + url
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)
448        
449         if not b1 or not b2 or not b3 or not b4:
450             return -1;
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