| 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 | # Copyright (C) 2016 Emmanuel Saint-James <esj@rezo.net> |
|---|
| 7 | # |
|---|
| 8 | |
|---|
| 9 | import mimetypes |
|---|
| 10 | import os |
|---|
| 11 | import re |
|---|
| 12 | from operator import itemgetter |
|---|
| 13 | |
|---|
| 14 | from .doxyfiletrac import init_doxyfile, post_doxyfile |
|---|
| 15 | from .saxygen import search_in_doxygen |
|---|
| 16 | from trac.admin import IAdminPanelProvider |
|---|
| 17 | from trac.config import Option |
|---|
| 18 | from trac.core import * |
|---|
| 19 | from trac.loader import get_plugin_info |
|---|
| 20 | from trac.perm import IPermissionRequestor |
|---|
| 21 | from trac.search.api import ISearchSource, shorten_result |
|---|
| 22 | from trac.util.datefmt import to_datetime |
|---|
| 23 | from trac.util.html import Markup, tag |
|---|
| 24 | from trac.util.text import to_unicode |
|---|
| 25 | from trac.util.translation import _ |
|---|
| 26 | from trac.web.api import IRequestHandler |
|---|
| 27 | from trac.web.chrome import INavigationContributor, ITemplateProvider, \ |
|---|
| 28 | add_ctxtnav, add_script, add_stylesheet, \ |
|---|
| 29 | web_context |
|---|
| 30 | from trac.wiki.api import IWikiSyntaxProvider, WikiSystem |
|---|
| 31 | from trac.wiki.formatter import format_to_html |
|---|
| 32 | from trac.wiki.model import WikiPage |
|---|
| 33 | |
|---|
| 34 | |
|---|
| 35 | class DoxygenPlugin(Component): |
|---|
| 36 | |
|---|
| 37 | implements(IAdminPanelProvider, INavigationContributor, |
|---|
| 38 | IPermissionRequestor, IRequestHandler, ISearchSource, |
|---|
| 39 | ITemplateProvider, IWikiSyntaxProvider) |
|---|
| 40 | |
|---|
| 41 | base_path = Option('doxygen', 'path', '', |
|---|
| 42 | """Directory containing doxygen generated files (= OUTPUT_DIRECTORY). |
|---|
| 43 | """) |
|---|
| 44 | |
|---|
| 45 | input = Option('doxygen', 'input', '', |
|---|
| 46 | """Directory containing sources.""") |
|---|
| 47 | |
|---|
| 48 | default_doc = Option('doxygen', 'default_documentation', '', |
|---|
| 49 | """Default documentation project, relative to `[doxygen] path`. |
|---|
| 50 | When no explicit path is given in a documentation request, |
|---|
| 51 | this path will be prepended to the request before looking |
|---|
| 52 | for documentation files.""") |
|---|
| 53 | |
|---|
| 54 | html_output = Option('doxygen', 'html_output', 'html', |
|---|
| 55 | """Default documentation project suffix, as generated by Doxygen |
|---|
| 56 | using the HTML_OUTPUT Doxygen configuration setting.""") |
|---|
| 57 | |
|---|
| 58 | title = Option('doxygen', 'title', 'Doxygen', |
|---|
| 59 | """Title to use for the main navigation tab.""") |
|---|
| 60 | |
|---|
| 61 | index = Option('doxygen', 'index', 'index.html', |
|---|
| 62 | """Default index page to pick in the generated documentation.""") |
|---|
| 63 | |
|---|
| 64 | searchdata_file = Option('doxygen', 'searchdata_file', 'searchdata.xml', |
|---|
| 65 | """Default name of XML search file.""") |
|---|
| 66 | |
|---|
| 67 | wiki_index = Option('doxygen', 'wiki_index', None, |
|---|
| 68 | """Wiki page to use as the default page for the Doxygen main page. |
|---|
| 69 | If set, supersedes the `[doxygen] index` option.""") |
|---|
| 70 | |
|---|
| 71 | encoding = Option('doxygen', 'encoding', 'utf-8', |
|---|
| 72 | """Default encoding used by the generated documentation files.""") |
|---|
| 73 | |
|---|
| 74 | default_namespace = Option('doxygen', 'default_namespace', '', |
|---|
| 75 | """Default namespace to search for named objects in.""") |
|---|
| 76 | |
|---|
| 77 | doxyfile = Option('doxygen', 'doxyfile', '', |
|---|
| 78 | """Full path of the Doxyfile to be created.""") |
|---|
| 79 | |
|---|
| 80 | doxygen = Option('doxygen', 'doxygen', '/usr/local/bin/doxygen', |
|---|
| 81 | """Full path of the Doxygen command.""") |
|---|
| 82 | |
|---|
| 83 | doxygen_args = Option('doxygen', 'doxygen_args', '', |
|---|
| 84 | """Arguments for the Doxygen command.""") |
|---|
| 85 | |
|---|
| 86 | def link_me(self, name): |
|---|
| 87 | for plugin in get_plugin_info(self.env): |
|---|
| 88 | if 'name' in plugin and plugin['name'] == name: |
|---|
| 89 | info = plugin['info'] |
|---|
| 90 | url = info.get('home_page') |
|---|
| 91 | version = info['version'] |
|---|
| 92 | return '<a href="' + url + '">TracDoxygen ' + version + '</a>' |
|---|
| 93 | return name |
|---|
| 94 | |
|---|
| 95 | def check_documentation(self, doc): |
|---|
| 96 | index = os.path.join(self.base_path, doc, self.searchdata_file) |
|---|
| 97 | if not os.path.exists(index) or not os.access(index, os.R_OK): |
|---|
| 98 | self.log.debug('No readable file "%s" in Doxygen dir ', index) |
|---|
| 99 | return '' |
|---|
| 100 | return index |
|---|
| 101 | |
|---|
| 102 | def merge_header(self, req, path): |
|---|
| 103 | """Split a Doxygen HTML page in its head and body part. |
|---|
| 104 | Find the references to style sheets by the Link tag |
|---|
| 105 | and move them to the Trac Head part by add_stylesheet. |
|---|
| 106 | Same work for the JS files referenced by the Script Tag, by |
|---|
| 107 | add_script. Move also the content of the Title Tag, by using JQuery. |
|---|
| 108 | """ |
|---|
| 109 | |
|---|
| 110 | try: |
|---|
| 111 | content = file(path).read() |
|---|
| 112 | except (IOError, OSError) as e: |
|---|
| 113 | raise TracError("Can't read doxygen content: %s" % e) |
|---|
| 114 | |
|---|
| 115 | m = re.match( |
|---|
| 116 | r'''^\s*<!DOCTYPE[^>]*>\s*<html[^>]*>\s*<head>(.*?)</head>\s*<body[^>]*>(.*)</body>\s*</html>''', |
|---|
| 117 | content, re.S) |
|---|
| 118 | |
|---|
| 119 | if not m: |
|---|
| 120 | return content |
|---|
| 121 | |
|---|
| 122 | # pick up links to CSS and move them to header of the Trac Page |
|---|
| 123 | l = re.findall(r'''<link[^>]*type=.text/css[^>]*>''', m.group(1), |
|---|
| 124 | re.S) |
|---|
| 125 | for i in l: |
|---|
| 126 | h = re.search(r'''href=.([^ ]*)[^ /][ /]''', i) |
|---|
| 127 | h = h.group(1) |
|---|
| 128 | u = re.match(r'''^[./]*([^:]+)$''', h) |
|---|
| 129 | if u: |
|---|
| 130 | h = os.path.join('/doxygen', u.group(1)) |
|---|
| 131 | add_stylesheet(req, h) |
|---|
| 132 | |
|---|
| 133 | # pick up the title of the Doxygen page |
|---|
| 134 | # since there is no API to move it in the header of the Trac page |
|---|
| 135 | # we will use JQuery to do it on load |
|---|
| 136 | t = re.search(r'''<title>.*?:(.*)</title>''', m.group(1), re.S) |
|---|
| 137 | if t: |
|---|
| 138 | t = '$(document).ready(function() { document.title+="' + t.group( |
|---|
| 139 | 1) + '";})' |
|---|
| 140 | else: |
|---|
| 141 | t = '' |
|---|
| 142 | # pick up the scripts |
|---|
| 143 | # if it is a file, move the tag Script in the header of the Trac page |
|---|
| 144 | # otherwise, keep it here |
|---|
| 145 | s = re.findall(r'''<script([^>]*)>(.*?)</script>''', m.group(1), re.S) |
|---|
| 146 | for i in s: |
|---|
| 147 | h = re.search(r'''src=.([^ ]*).''', i[0]) |
|---|
| 148 | if not h: |
|---|
| 149 | t += i[1] |
|---|
| 150 | else: |
|---|
| 151 | h = h.group(1) |
|---|
| 152 | if h != 'jquery.js': |
|---|
| 153 | u = re.match(r'''^[./]*([^:]+)$''', h) |
|---|
| 154 | if u: |
|---|
| 155 | h = os.path.join('/doxygen', u.group(1)) |
|---|
| 156 | add_script(req, h) |
|---|
| 157 | |
|---|
| 158 | if t: |
|---|
| 159 | t = "<script type='application/javascript'>" + t + "</script>\n" |
|---|
| 160 | return t + m.group(2) |
|---|
| 161 | |
|---|
| 162 | def rewrite_doxygen(self, req, path, doc, charset): |
|---|
| 163 | def wiki_in_doxygen(m): |
|---|
| 164 | context = web_context(req) |
|---|
| 165 | return format_to_html(self.env, context, m.group(1)) |
|---|
| 166 | |
|---|
| 167 | content = to_unicode(self.merge_header(req, path), charset) |
|---|
| 168 | |
|---|
| 169 | # Add a query string for explicit documentation |
|---|
| 170 | if doc: |
|---|
| 171 | href = re.compile(r'''<a.*?href=.[^"]*?[.]html''') |
|---|
| 172 | content = href.sub(r'\g<0>' + '?doc=' + doc, content) |
|---|
| 173 | |
|---|
| 174 | # translate TracLink in Doxygen comments |
|---|
| 175 | # (unless some HTML tags are present. Should be better) |
|---|
| 176 | comment = re.compile(r'''<p>([^<>&]*?)</p>''', re.S) |
|---|
| 177 | content = comment.sub(wiki_in_doxygen, content) |
|---|
| 178 | comment = re.compile(r'''<dd>([^<>&]*?)</dd>''', re.S) |
|---|
| 179 | content = comment.sub(wiki_in_doxygen, content) |
|---|
| 180 | |
|---|
| 181 | name = self.link_me('TracDoxygen') |
|---|
| 182 | content = re.sub(r'(<small>.*)(<a .*</small>)', |
|---|
| 183 | r'\1' + name + r' & \2', content, 1, re.S) |
|---|
| 184 | return {'doxygen_content': Markup(content)} |
|---|
| 185 | |
|---|
| 186 | # IPermissionRequestor methods |
|---|
| 187 | |
|---|
| 188 | def get_permission_actions(self): |
|---|
| 189 | return ['DOXYGEN_VIEW'] |
|---|
| 190 | |
|---|
| 191 | # INavigationContributor methods |
|---|
| 192 | |
|---|
| 193 | def get_active_navigation_item(self, req): |
|---|
| 194 | return 'doxygen' |
|---|
| 195 | |
|---|
| 196 | def get_navigation_items(self, req): |
|---|
| 197 | if 'DOXYGEN_VIEW' in req.perm: |
|---|
| 198 | # Return mainnav buttons. |
|---|
| 199 | yield ('mainnav', 'doxygen', |
|---|
| 200 | tag.a(self.title, href=req.href.doxygen())) |
|---|
| 201 | |
|---|
| 202 | # IRequestHandler methods |
|---|
| 203 | |
|---|
| 204 | def match_request(self, req): |
|---|
| 205 | return re.match(r'/doxygen(/|$)', req.path_info) |
|---|
| 206 | |
|---|
| 207 | def process_request(self, req): |
|---|
| 208 | req.perm.assert_permission('DOXYGEN_VIEW') |
|---|
| 209 | if req.path_info == '/doxygen': |
|---|
| 210 | req.redirect(req.href.doxygen('/')) |
|---|
| 211 | |
|---|
| 212 | segments = [_f for _f in req.path_info.split('/') if _f] |
|---|
| 213 | segments = segments[1:] # ditch 'doxygen' |
|---|
| 214 | if not segments: |
|---|
| 215 | # Handle /doxygen request |
|---|
| 216 | wiki = self.wiki_index |
|---|
| 217 | if wiki: |
|---|
| 218 | if WikiSystem(self.env).has_page(wiki): |
|---|
| 219 | text = WikiPage(self.env, wiki).text |
|---|
| 220 | else: |
|---|
| 221 | text = 'Doxygen index page [wiki:%s] does not exist.' % \ |
|---|
| 222 | wiki |
|---|
| 223 | context = web_context(req) |
|---|
| 224 | data = {'doxygen_text': format_to_html(self.env, context, text)} |
|---|
| 225 | add_ctxtnav(req, "View %s page" % wiki, req.href.wiki(wiki)) |
|---|
| 226 | return 'doxygen.html', data, 'text/html' |
|---|
| 227 | else: |
|---|
| 228 | # use configured Doxygen index |
|---|
| 229 | file_ = self.index |
|---|
| 230 | dir_ = '' |
|---|
| 231 | else: |
|---|
| 232 | file_ = segments[-1] |
|---|
| 233 | dir_ = segments[:-1] |
|---|
| 234 | dir_ = os.path.join(*dir_) if dir_ else '' |
|---|
| 235 | |
|---|
| 236 | doc = req.args.get('doc') if req.args.get('doc') else self.default_doc |
|---|
| 237 | path = os.path.join(self.base_path, doc, self.html_output, dir_, |
|---|
| 238 | file_) |
|---|
| 239 | if not path or not os.path.exists(path): |
|---|
| 240 | self.log.debug('%s not found in %s for doc %s', file_, path, doc) |
|---|
| 241 | url = req.href.search(q=req.args.get('query'), doxygen='on') |
|---|
| 242 | req.redirect(url) |
|---|
| 243 | |
|---|
| 244 | # security check |
|---|
| 245 | path = os.path.abspath(path) |
|---|
| 246 | if not path.startswith(os.path.normpath(self.base_path)): |
|---|
| 247 | raise TracError("Can't access paths outside of " + self.base_path) |
|---|
| 248 | |
|---|
| 249 | mimetype = mimetypes.guess_type(path)[0] |
|---|
| 250 | if mimetype == 'text/html': |
|---|
| 251 | add_stylesheet(req, 'doxygen/css/doxygen.css') |
|---|
| 252 | charset = (self.encoding or |
|---|
| 253 | self.env.config['trac'].get('default_charset')) |
|---|
| 254 | doc = doc if req.args.get('doc') else '' |
|---|
| 255 | content = self.rewrite_doxygen(req, path, doc, charset) |
|---|
| 256 | return 'doxygen.html', content, 'text/html' |
|---|
| 257 | else: |
|---|
| 258 | req.send_file(path, mimetype) |
|---|
| 259 | |
|---|
| 260 | # ITemplateProvider methods |
|---|
| 261 | |
|---|
| 262 | def get_htdocs_dirs(self): |
|---|
| 263 | from pkg_resources import resource_filename |
|---|
| 264 | return [('doxygen', resource_filename(__name__, 'htdocs'))] |
|---|
| 265 | |
|---|
| 266 | def get_templates_dirs(self): |
|---|
| 267 | from pkg_resources import resource_filename |
|---|
| 268 | return [resource_filename(__name__, 'templates')] |
|---|
| 269 | |
|---|
| 270 | # IAdminPanelProvidermethods |
|---|
| 271 | |
|---|
| 272 | def get_admin_panels(self, req): |
|---|
| 273 | if 'TRAC_ADMIN' in req.perm: |
|---|
| 274 | yield ('general', _("General"), 'query', 'Doxyfile') |
|---|
| 275 | |
|---|
| 276 | def render_admin_panel(self, req, cat, page, info): |
|---|
| 277 | req.perm.require('TRAC_ADMIN') |
|---|
| 278 | |
|---|
| 279 | if req.method == 'POST': |
|---|
| 280 | # if post is ok, we dont return here: |
|---|
| 281 | # a redirection to the main page of the documentation occurs |
|---|
| 282 | env = post_doxyfile(req, self.doxygen, self.doxygen_args, |
|---|
| 283 | self.doxyfile, self.input, self.base_path, |
|---|
| 284 | self.log) |
|---|
| 285 | else: |
|---|
| 286 | env = {} |
|---|
| 287 | |
|---|
| 288 | env = init_doxyfile(env, self.doxygen, self.doxyfile, self.input, |
|---|
| 289 | self.base_path, self.default_doc, self.log) |
|---|
| 290 | add_stylesheet(req, 'doxygen/css/doxygen.css') |
|---|
| 291 | add_script(req, 'doxygen/js/doxygentrac.js') |
|---|
| 292 | return 'doxygen_admin.html', env, None |
|---|
| 293 | |
|---|
| 294 | # ISearchProvider methods |
|---|
| 295 | |
|---|
| 296 | def get_search_filters(self, req): |
|---|
| 297 | if 'DOXYGEN_VIEW' not in req.perm: |
|---|
| 298 | return |
|---|
| 299 | if not self.check_documentation(self.default_doc): |
|---|
| 300 | return |
|---|
| 301 | yield 'doxygen', self.title |
|---|
| 302 | |
|---|
| 303 | def get_search_results(self, req, keywords, filters): |
|---|
| 304 | """Return the entry whose 'keyword' or 'text' tag contains |
|---|
| 305 | one or more word among the keywords. |
|---|
| 306 | """ |
|---|
| 307 | |
|---|
| 308 | if 'doxygen' not in filters: |
|---|
| 309 | return |
|---|
| 310 | |
|---|
| 311 | k = '|'.join(keywords).encode(self.encoding) |
|---|
| 312 | doc = self.check_documentation(self.default_doc) |
|---|
| 313 | all_ = search_in_doxygen(doc, k, ['keywords', 'text'], True, self.log) |
|---|
| 314 | all_ = sorted(all_, key=itemgetter('keywords')) |
|---|
| 315 | all_ = sorted(all_, key=itemgetter('occ'), reverse=True) |
|---|
| 316 | for res in all_: |
|---|
| 317 | url = 'doxygen/' + res['url'] + '#' + res['target'] |
|---|
| 318 | t = shorten_result(res['text']) |
|---|
| 319 | yield url, res['keywords'], to_datetime(res['date']), 'doxygen', t |
|---|
| 320 | |
|---|
| 321 | # IWikiSyntaxProvider |
|---|
| 322 | |
|---|
| 323 | def get_link_resolvers(self): |
|---|
| 324 | def doxygen_link(formatter, ns, name, label): |
|---|
| 325 | doc = self.default_doc |
|---|
| 326 | if '/' in name: |
|---|
| 327 | doc, name = name.split('/') |
|---|
| 328 | if not doc: |
|---|
| 329 | doc = self.default_doc |
|---|
| 330 | if not name: |
|---|
| 331 | if doc: |
|---|
| 332 | label = doc |
|---|
| 333 | else: |
|---|
| 334 | label = 'index' |
|---|
| 335 | res = {'url': 'index.html', 'target': '', 'type': 'file', |
|---|
| 336 | 'text': 'index'} |
|---|
| 337 | else: |
|---|
| 338 | file_ = self.check_documentation(doc) |
|---|
| 339 | res = search_in_doxygen(file_, name, ['name'], False, |
|---|
| 340 | self.log) |
|---|
| 341 | if not res: |
|---|
| 342 | suffix = '[.:\\\\]' + name + '$' |
|---|
| 343 | res = search_in_doxygen(file_, suffix, ['name'], True, |
|---|
| 344 | self.log) |
|---|
| 345 | if len(res) != 1: |
|---|
| 346 | self.log.debug('%s: %d occurrences in %s', suffix, |
|---|
| 347 | len(res), file_) |
|---|
| 348 | return tag.a(label, title=name, class_='missing', |
|---|
| 349 | href=formatter.href.doxygen()) |
|---|
| 350 | else: |
|---|
| 351 | res = res[0] |
|---|
| 352 | |
|---|
| 353 | if doc != self.default_doc: |
|---|
| 354 | url = formatter.href.doxygen(res['url'], doc=doc) |
|---|
| 355 | else: |
|---|
| 356 | url = formatter.href.doxygen(res['url']) |
|---|
| 357 | url += '#' + res['target'] |
|---|
| 358 | self.log.debug("doxygen_link %s for %s in %s", url, name, doc) |
|---|
| 359 | t = res['type'] |
|---|
| 360 | if t == 'function': |
|---|
| 361 | t += ' ' + res['name'] + ' ' + res['args'] |
|---|
| 362 | t += ' ' + shorten_result(res['text']) |
|---|
| 363 | return tag.a(label, title=t, href=url) |
|---|
| 364 | |
|---|
| 365 | yield 'doxygen', doxygen_link |
|---|
| 366 | |
|---|
| 367 | def get_wiki_syntax(self): |
|---|
| 368 | return [] |
|---|