File indexing completed on 2024-04-28 11:33:41
0001 # -*- coding: utf-8 -*- 0002 # 0003 # SPDX-FileCopyrightText: 2014 Alex Merry <alex.merry@kdemail.net> 0004 # SPDX-FileCopyrightText: 2014 Aurélien Gâteau <agateau@kde.org> 0005 # SPDX-FileCopyrightText: 2014 Alex Turbov <i.zaufi@gmail.com> 0006 # SPDX-FileCopyrightText: 2016 Olivier Churlaud <olivier@churlaud.com> 0007 # 0008 # SPDX-License-Identifier: BSD-2-Clause 0009 0010 import codecs 0011 from distutils.spawn import find_executable 0012 import datetime 0013 import os 0014 import logging 0015 from os import environ 0016 import shutil 0017 import subprocess 0018 import tempfile 0019 import sys 0020 import pathlib 0021 from typing import Any, Dict 0022 import xml.etree.ElementTree as ET 0023 import re 0024 import glob 0025 from pathlib import Path 0026 0027 import jinja2 0028 0029 from urllib.parse import urljoin 0030 0031 import xml.etree.ElementTree as xmlET 0032 import json 0033 0034 from jinja2.environment import Template 0035 0036 from kapidox import utils 0037 try: 0038 from kapidox import depdiagram 0039 DEPDIAGRAM_AVAILABLE = True 0040 except ImportError: 0041 DEPDIAGRAM_AVAILABLE = False 0042 0043 from .doxyfilewriter import DoxyfileWriter 0044 0045 0046 # @package kapidox.generator 0047 # 0048 # The generator 0049 0050 __all__ = ( 0051 "Context", 0052 "generate_apidocs", 0053 "search_for_tagfiles", 0054 "WARN_LOGFILE", 0055 "build_classmap", 0056 "postprocess", 0057 "create_dirs", 0058 "create_jinja_environment", 0059 ) 0060 0061 WARN_LOGFILE = 'doxygen-warnings.log' 0062 0063 HTML_SUBDIR = 'html' 0064 0065 0066 class Context(object): 0067 """ 0068 Holds parameters used by the various functions of the generator 0069 """ 0070 __slots__ = ( 0071 # Names 0072 'modulename', 0073 'fancyname', 0074 'title', 0075 'fwinfo', 0076 # KApidox files 0077 'doxdatadir', 0078 'resourcedir', 0079 # Input 0080 'srcdir', 0081 'tagfiles', 0082 'dependency_diagram', 0083 'copyright', 0084 'is_qdoc', 0085 # Output 0086 'outputdir', 0087 'htmldir', 0088 'tagfile', 0089 # Output options 0090 'man_pages', 0091 'qhp', 0092 # Binaries 0093 'doxygen', 0094 'qhelpgenerator', 0095 ) 0096 0097 def __init__(self, args, **kwargs): 0098 # Names 0099 self.title = args.title 0100 # KApidox files 0101 self.doxdatadir = args.doxdatadir 0102 # Output options 0103 self.man_pages = args.man_pages 0104 self.qhp = args.qhp 0105 # Binaries 0106 self.doxygen = args.doxygen 0107 self.qhelpgenerator = args.qhelpgenerator 0108 0109 for key in self.__slots__: 0110 if not hasattr(self, key): 0111 setattr(self, key, kwargs.get(key)) 0112 0113 0114 def create_jinja_environment(doxdatadir): 0115 loader = jinja2.FileSystemLoader(os.path.join(doxdatadir, 'templates')) 0116 return jinja2.Environment(loader=loader) 0117 0118 0119 def process_toplevel_html_file(outputfile, doxdatadir, products, title, qch_enabled=False): 0120 0121 def sort_product(product): 0122 print(product.fancyname) 0123 if product.fancyname == "The KDE Frameworks": 0124 return 'aa' 0125 if product.fancyname == "KDE PIM": 0126 return 'ab' 0127 return product.fancyname.lower() 0128 0129 products.sort(key=sort_product) 0130 mapping = { 0131 'resources': './resources', 0132 # steal the doxygen css from one of the frameworks 0133 # this means that all the doxygen-provided images etc. will be found 0134 'title': title, 0135 'qch': qch_enabled, 0136 'breadcrumbs': { 0137 'entries': [ 0138 { 0139 'href': './index.html', 0140 'text': 'KDE API Reference' 0141 } 0142 ] 0143 }, 0144 'product_list': products, 0145 } 0146 tmpl = create_jinja_environment(doxdatadir).get_template('frontpage.html') 0147 with codecs.open(outputfile, 'w', 'utf-8') as outf: 0148 outf.write(tmpl.render(mapping)) 0149 0150 tmpl2 = create_jinja_environment(doxdatadir).get_template('search.html') 0151 search_output = "search.html" 0152 with codecs.open(search_output, 'w', 'utf-8') as outf: 0153 outf.write(tmpl2.render(mapping)) 0154 0155 0156 def process_subgroup_html_files(outputfile, doxdatadir, groups, available_platforms, title, qch_enabled=False): 0157 0158 for group in groups: 0159 mapping = { 0160 'resources': '../resources', 0161 'title': title, 0162 'qch': qch_enabled, 0163 'breadcrumbs': { 0164 'entries': [ 0165 { 0166 'href': '../index.html', 0167 'text': 'KDE API Reference' 0168 }, 0169 { 0170 'href': './index.html', 0171 'text': group.fancyname 0172 } 0173 ] 0174 }, 0175 'group': group, 0176 'available_platforms': sorted(available_platforms), 0177 } 0178 0179 if not os.path.isdir(group.name): 0180 os.mkdir(group.name) 0181 outputfile = group.name + '/index.html' 0182 tmpl = create_jinja_environment(doxdatadir).get_template('subgroup.html') 0183 with codecs.open(outputfile, 'w', 'utf-8') as outf: 0184 outf.write(tmpl.render(mapping)) 0185 0186 tmpl2 = create_jinja_environment(doxdatadir).get_template('search.html') 0187 search_output = group.name + "/search.html" 0188 with codecs.open(search_output, 'w', 'utf-8') as outf: 0189 outf.write(tmpl2.render(mapping)) 0190 0191 0192 def create_dirs(ctx): 0193 ctx.htmldir = os.path.join(ctx.outputdir, HTML_SUBDIR) 0194 ctx.tagfile = os.path.join(ctx.htmldir, ctx.fwinfo.fancyname + '.tags') 0195 0196 if not os.path.exists(ctx.outputdir): 0197 os.makedirs(ctx.outputdir) 0198 0199 if os.path.exists(ctx.htmldir): 0200 # If we have files left there from a previous run but which are no 0201 # longer generated (for example because a C++ class has been removed) 0202 # then postprocess will fail because the left-over file has already been 0203 # processed. To avoid that, we delete the html dir. 0204 shutil.rmtree(ctx.htmldir) 0205 os.makedirs(ctx.htmldir) 0206 0207 0208 def load_template(path): 0209 # Set errors to 'ignore' because we don't want weird characters in Doxygen 0210 # output (for example source code listing) to cause a failure 0211 content = codecs.open(path, encoding='utf-8', errors='ignore').read() 0212 try: 0213 return jinja2.Template(content) 0214 except jinja2.exceptions.TemplateSyntaxError: 0215 logging.error('Failed to parse template {}'.format(path)) 0216 raise 0217 0218 0219 def find_tagfiles(docdir, doclink=None, flattenlinks=False, exclude=None, _depth=0): 0220 """Find Doxygen-generated tag files in a directory. 0221 0222 The tag files must have the extension .tags, and must be in the listed 0223 directory, a subdirectory or a subdirectory named html of a subdirectory. 0224 0225 Args: 0226 docdir: (string) the directory to search. 0227 doclink: (string) the path or URL to use when creating the 0228 documentation links; if None, this will default to 0229 docdir. (optional, default None) 0230 flattenlinks: (bool) if True, generated links will assume all the html 0231 files are directly under doclink; else the html files are 0232 assumed to be at the same relative location to doclink as 0233 the tag file is to docdir; ignored if doclink is not set. 0234 (optional, default False) 0235 0236 Returns: 0237 A list of pairs of (tag_file,link_path). 0238 """ 0239 0240 if not os.path.isdir(docdir): 0241 return [] 0242 0243 if doclink is None: 0244 doclink = docdir 0245 flattenlinks = False 0246 0247 def smartjoin(pathorurl1, *args): 0248 """Join paths or URLS 0249 0250 It figures out which it is from whether the first contains a "://" 0251 """ 0252 if '://' in pathorurl1: 0253 if not pathorurl1.endswith('/'): 0254 pathorurl1 += '/' 0255 return urljoin(pathorurl1, *args) 0256 else: 0257 return os.path.join(pathorurl1, *args) 0258 0259 def nestedlink(subdir): 0260 if flattenlinks: 0261 return doclink 0262 else: 0263 return smartjoin(doclink, subdir) 0264 0265 tagfiles = [] 0266 0267 entries = os.listdir(docdir) 0268 for e in entries: 0269 if e == exclude: 0270 continue 0271 path = os.path.join(docdir, e) 0272 if os.path.isfile(path) and e.endswith('.tags'): 0273 tagfiles.append((path, doclink)) 0274 elif (_depth == 0 or (_depth == 1 and e == 'html')) and os.path.isdir(path): 0275 tagfiles += find_tagfiles(path, nestedlink(e), 0276 flattenlinks=flattenlinks, 0277 _depth=_depth+1, 0278 exclude=exclude) 0279 0280 return tagfiles 0281 0282 0283 def search_for_tagfiles(suggestion=None, doclink=None, flattenlinks=False, searchpaths=[], exclude=None): 0284 """Find Doxygen-generated tag files 0285 0286 See the find_tagfiles documentation for how the search is carried out in 0287 each directory; this just allows a list of directories to be searched. 0288 0289 At least one of docdir or searchpaths must be given for it to find anything. 0290 0291 Args: 0292 suggestion: the first place to look (will complain if there are no 0293 documentation tag files there) 0294 doclink: the path or URL to use when creating the documentation 0295 links; if None, this will default to docdir 0296 flattenlinks: if this is True, generated links will assume all the html 0297 files are directly under doclink; if False (the default), 0298 the html files are assumed to be at the same relative 0299 location to doclink as the tag file is to docdir; ignored 0300 if doclink is not set 0301 searchpaths: other places to look for documentation tag files 0302 0303 Returns: 0304 A list of pairs of (tag_file,link_path) 0305 """ 0306 0307 if suggestion is not None: 0308 if not os.path.isdir(suggestion): 0309 logging.warning(suggestion + " is not a directory") 0310 else: 0311 tagfiles = find_tagfiles(suggestion, doclink, flattenlinks, exclude) 0312 if len(tagfiles) == 0: 0313 logging.warning(suggestion + " does not contain any tag files") 0314 else: 0315 return tagfiles 0316 0317 for d in searchpaths: 0318 tagfiles = find_tagfiles(d, doclink, flattenlinks, exclude) 0319 if len(tagfiles) > 0: 0320 logging.info("Documentation tag files found at " + d) 0321 return tagfiles 0322 0323 return [] 0324 0325 0326 def menu_items(htmldir, modulename): 0327 """Menu items for standard Doxygen files 0328 0329 Looks for a set of standard Doxygen files (like namespaces.html) and 0330 provides menu text for those it finds in htmldir. 0331 0332 Args: 0333 htmldir: (string) the directory the HTML files are contained in. 0334 modulename: (string) the name of the library 0335 0336 Returns: 0337 A list of maps with 'text' and 'href' keys. 0338 """ 0339 entries = [ 0340 {'text': 'Main Page', 'href': 'index.html'}, 0341 {'text': 'Namespace List', 'href': 'namespaces.html'}, 0342 {'text': 'Namespace Members', 'href': 'namespacemembers.html'}, 0343 {'text': 'Alphabetical List', 'href': 'classes.html'}, 0344 {'text': 'Class List', 'href': 'annotated.html'}, 0345 {'text': 'Class Hierarchy', 'href': 'hierarchy.html'}, 0346 {'text': 'File List', 'href': 'files.html'}, 0347 {'text': 'File Members', 'href': 'globals.html'}, 0348 {'text': 'Modules', 'href': 'modules.html'}, 0349 {'text': 'Directories', 'href': 'dirs.html'}, 0350 {'text': 'Dependencies', 'href': modulename + '-dependencies.html'}, 0351 {'text': 'Related Pages', 'href': 'pages.html'}, 0352 ] 0353 # NOTE In Python 3 filter() builtin returns an iterable, but not a list 0354 # type, so explicit conversion is here! 0355 return list(filter( 0356 lambda e: os.path.isfile(os.path.join(htmldir, e['href'])), 0357 entries)) 0358 0359 0360 def parse_dox_html(stream): 0361 """Parse the HTML files produced by Doxygen, extract the key/value block we 0362 add through header.html and return a dict ready for the Jinja template. 0363 0364 The HTML files produced by Doxygen with our custom header and footer files 0365 look like this: 0366 0367 @code 0368 <!-- 0369 key1: value1 0370 key2: value2 0371 ... 0372 --> 0373 <html> 0374 <head> 0375 ... 0376 </head> 0377 <body> 0378 ... 0379 </body> 0380 </html> 0381 @endcode 0382 0383 The parser fills the dict from the top key/value block, and add the content 0384 of the body to the dict using the "content" key. 0385 0386 We do not use an XML parser because the HTML file might not be well-formed, 0387 for example if the documentation contains raw HTML. 0388 0389 The key/value block is kept in a comment so that it does not appear in Qt 0390 Compressed Help output, which is not post processed by ourself. 0391 """ 0392 dct = {} 0393 body = [] 0394 0395 def parse_key_value_block(line): 0396 if line == "<!--": 0397 return parse_key_value_block 0398 if line == "-->": 0399 return skip_head 0400 key, value = line.split(': ', 1) 0401 dct[key] = value 0402 return parse_key_value_block 0403 0404 def skip_head(line): 0405 if line == "<body>": 0406 return extract_body 0407 else: 0408 return skip_head 0409 0410 def extract_body(line): 0411 if line == "</body>": 0412 return None 0413 body.append(line) 0414 return extract_body 0415 0416 parser = parse_key_value_block 0417 while parser is not None: 0418 line = stream.readline().rstrip() 0419 parser = parser(line) 0420 0421 dct['content'] = '\n'.join(body) 0422 return dct 0423 0424 0425 def postprocess_internal_qdoc(htmldir: str, tmpl: Template, env: Dict[str, Any]): 0426 """Substitute text in HTML files 0427 0428 Performs text substitutions on each line in each .html file in a directory. 0429 0430 Args: 0431 htmldir: (string) the directory containing the .html files. 0432 mapping: (dict) a dict of mappings. 0433 0434 """ 0435 for path in glob.glob(os.path.join(htmldir, "*.html")): 0436 newpath = f"{path}.new" 0437 0438 txt = Path(path).read_text() 0439 env['docs'] = txt.partition('body')[2].partition('</body>')[0] 0440 0441 with codecs.open(newpath, 'w', 'utf-8') as outf: 0442 try: 0443 html = tmpl.render(env) 0444 except BaseException: 0445 logging.error(f"Postprocessing {path} failed") 0446 raise 0447 0448 outf.write(html) 0449 0450 os.remove(path) 0451 os.rename(newpath, path) 0452 0453 0454 def postprocess_internal(htmldir, tmpl, mapping): 0455 """Substitute text in HTML files 0456 0457 Performs text substitutions on each line in each .html file in a directory. 0458 0459 Args: 0460 htmldir: (string) the directory containing the .html files. 0461 mapping: (dict) a dict of mappings. 0462 0463 """ 0464 for name in os.listdir(htmldir): 0465 if name.endswith('.html'): 0466 path = os.path.join(htmldir, name) 0467 newpath = path + '.new' 0468 0469 if name != 'classes.html' and name.startswith('class'): 0470 mapping['classname'] = name[5:-5].split('_1_1')[-1] 0471 mapping['fullname'] = name[5:-5].replace('_1_1', '::') 0472 elif name.startswith('namespace') and name != 'namespaces.html' and not name.startswith('namespacemembers'): 0473 mapping['classname'] = None 0474 mapping['fullname'] = name[9:-5].replace('_1_1', '::') 0475 else: 0476 mapping['classname'] = None 0477 mapping['fullname'] = None 0478 0479 with codecs.open(path, 'r', 'utf-8', errors='ignore') as f: 0480 mapping['dox'] = parse_dox_html(f) 0481 0482 with codecs.open(newpath, 'w', 'utf-8') as outf: 0483 try: 0484 html = tmpl.render(mapping) 0485 except Exception: 0486 logging.error('postprocessing {} failed'.format(path)) 0487 raise 0488 outf.write(html) 0489 os.remove(path) 0490 os.rename(newpath, path) 0491 0492 0493 def build_classmap(tagfile): 0494 """Parses a tagfile to get a map from classes to files 0495 0496 Args: 0497 tagfile: the Doxygen-generated tagfile to parse. 0498 0499 Returns: 0500 A list of maps (keys: classname and filename). 0501 """ 0502 import xml.etree.ElementTree as ET 0503 tree = ET.parse(tagfile) 0504 tagfile_root = tree.getroot() 0505 mapping = [] 0506 for compound in tagfile_root: 0507 kind = compound.get('kind') 0508 if kind == 'class' or kind == 'namespace': 0509 name_el = compound.find('name') 0510 filename_el = compound.find('filename') 0511 mapping.append({'classname': name_el.text, 0512 'filename': filename_el.text}) 0513 return mapping 0514 0515 0516 def generate_dependencies_page(tmp_dir, doxdatadir, modulename, dependency_diagram): 0517 """Create `modulename`-dependencies.md in `tmp_dir`""" 0518 template_path = os.path.join(doxdatadir, 'dependencies.md.tmpl') 0519 out_path = os.path.join(tmp_dir, modulename + '-dependencies.md') 0520 tmpl = load_template(template_path) 0521 with codecs.open(out_path, 'w', 'utf-8') as outf: 0522 txt = tmpl.render({ 0523 'modulename': modulename, 0524 'diagramname': os.path.basename(dependency_diagram), 0525 }) 0526 outf.write(txt) 0527 return out_path 0528 0529 0530 def generate_apidocs_qdoc(ctx: Context, tmp_dir: str, doxyfile_entries=None, keep_temp_dirs=False): 0531 absolute = pathlib.Path(os.path.join(ctx.outputdir, 'html')).absolute() 0532 0533 environ['KAPIDOX_DIR'] = ctx.doxdatadir 0534 0535 logging.info(f'Running QDoc (qdoc {ctx.fwinfo.path}/.qdocconf --outputdir={absolute}') 0536 ret = subprocess.call(['qdoc', ctx.fwinfo.path + "/.qdocconf", f"--outputdir={absolute}"]) 0537 if ret != 0: 0538 raise Exception("QDoc exited with a non-zero status code") 0539 0540 0541 def generate_apidocs(ctx: Context, tmp_dir, doxyfile_entries=None, keep_temp_dirs=False): 0542 """Generate the API documentation for a single directory""" 0543 0544 if ctx.is_qdoc: 0545 return generate_apidocs_qdoc(ctx, tmp_dir, doxyfile_entries, keep_temp_dirs) 0546 0547 def find_src_subdir(dirlist, deeper_subd=None): 0548 returnlist = [] 0549 for d in dirlist: 0550 pth = os.path.join(ctx.fwinfo.path, d) 0551 if deeper_subd is not None: 0552 pth = os.path.join(pth, deeper_subd) 0553 if os.path.isdir(pth) or os.path.isfile(pth): 0554 returnlist.append(pth) 0555 else: 0556 pass # We drop it 0557 return returnlist 0558 0559 input_list = [] 0560 if os.path.isfile(ctx.fwinfo.path + "/Mainpage.dox"): 0561 input_list.append(ctx.fwinfo.path + "/Mainpage.dox") 0562 elif os.path.isfile(ctx.fwinfo.path + "/README.md"): 0563 input_list.append(ctx.fwinfo.path + "/README.md") 0564 0565 input_list.extend(find_src_subdir(ctx.fwinfo.srcdirs)) 0566 input_list.extend(find_src_subdir(ctx.fwinfo.docdir)) 0567 image_path_list = [] 0568 0569 if ctx.dependency_diagram: 0570 input_list.append(generate_dependencies_page(tmp_dir, 0571 ctx.doxdatadir, 0572 ctx.modulename, 0573 ctx.dependency_diagram)) 0574 image_path_list.append(ctx.dependency_diagram) 0575 0576 doxyfile_path = os.path.join(tmp_dir, 'Doxyfile') 0577 with codecs.open(doxyfile_path, 'w', 'utf-8') as doxyfile: 0578 # Global defaults 0579 with codecs.open(os.path.join(ctx.doxdatadir, 'Doxyfile.global'), 0580 'r', 'utf-8') as f: 0581 for line in f: 0582 doxyfile.write(line) 0583 0584 writer = DoxyfileWriter(doxyfile) 0585 writer.write_entry('PROJECT_NAME', ctx.fancyname) 0586 # FIXME: can we get the project version from CMake? No from GIT TAGS! 0587 0588 # Input locations 0589 image_path_list.extend(find_src_subdir(ctx.fwinfo.docdir, 'pics')) 0590 writer.write_entries( 0591 INPUT=input_list, 0592 DOTFILE_DIRS=find_src_subdir(ctx.fwinfo.docdir, 'dot'), 0593 EXAMPLE_PATH=find_src_subdir(ctx.fwinfo.exampledirs), 0594 IMAGE_PATH=image_path_list) 0595 0596 # Other input settings 0597 writer.write_entry('TAGFILES', [f + '=' + loc for f, loc in ctx.tagfiles]) 0598 0599 # Output locations 0600 writer.write_entries( 0601 OUTPUT_DIRECTORY=ctx.outputdir, 0602 GENERATE_TAGFILE=ctx.tagfile, 0603 HTML_OUTPUT=HTML_SUBDIR, 0604 WARN_LOGFILE=os.path.join(ctx.outputdir, WARN_LOGFILE)) 0605 0606 # Other output settings 0607 writer.write_entries( 0608 HTML_HEADER=ctx.doxdatadir + '/header.html', 0609 HTML_FOOTER=ctx.doxdatadir + '/footer.html' 0610 ) 0611 0612 # Set a layout so that properties are first 0613 writer.write_entries( 0614 LAYOUT_FILE=ctx.doxdatadir + '/DoxygenLayout.xml' 0615 ) 0616 0617 # Always write these, even if QHP is disabled, in case Doxygen.local 0618 # overrides it 0619 writer.write_entries( 0620 QHP_VIRTUAL_FOLDER=ctx.modulename, 0621 QHP_NAMESPACE="org.kde." + ctx.modulename, 0622 QHG_LOCATION=ctx.qhelpgenerator) 0623 0624 writer.write_entries( 0625 GENERATE_MAN=ctx.man_pages, 0626 GENERATE_QHP=ctx.qhp) 0627 0628 if doxyfile_entries: 0629 writer.write_entries(**doxyfile_entries) 0630 0631 # Module-specific overrides 0632 if find_src_subdir(ctx.fwinfo.docdir): 0633 localdoxyfile = os.path.join(find_src_subdir(ctx.fwinfo.docdir)[0], 'Doxyfile.local') 0634 if os.path.isfile(localdoxyfile): 0635 with codecs.open(localdoxyfile, 'r', 'utf-8') as f: 0636 for line in f: 0637 doxyfile.write(line) 0638 0639 logging.info('Running Doxygen') 0640 subprocess.call([ctx.doxygen, doxyfile_path]) 0641 0642 0643 def generate_diagram(png_path, fancyname, dot_files, tmp_dir): 0644 """Generate a dependency diagram for a framework. 0645 """ 0646 def run_cmd(cmd, **kwargs): 0647 try: 0648 subprocess.check_call(cmd, **kwargs) 0649 except subprocess.CalledProcessError as exc: 0650 logging.error(f'Command {exc.cmd} failed with error code {exc.returncode}.') 0651 return False 0652 return True 0653 0654 logging.info('Generating dependency diagram') 0655 dot_path = os.path.join(tmp_dir, fancyname + '.dot') 0656 0657 with open(dot_path, 'w') as f: 0658 with_qt = False 0659 ok = depdiagram.generate(f, dot_files, framework=fancyname, 0660 with_qt=with_qt) 0661 if not ok: 0662 logging.error('Generating diagram failed') 0663 return False 0664 0665 logging.info('- Simplifying diagram') 0666 simplified_dot_path = os.path.join(tmp_dir, fancyname + '-simplified.dot') 0667 with open(simplified_dot_path, 'w') as f: 0668 if not run_cmd(['tred', dot_path], stdout=f): 0669 return False 0670 0671 logging.info('- Generating diagram png') 0672 if not run_cmd(['dot', '-Tpng', '-o' + png_path, simplified_dot_path]): 0673 return False 0674 0675 # These os.unlink() calls are not in a 'finally' block on purpose. 0676 # Keeping the dot files around makes it possible to inspect their content 0677 # when running with the --keep-temp-dirs option. If generation fails and 0678 # --keep-temp-dirs is not set, the files will be removed when the program 0679 # ends because they were created in `tmp_dir`. 0680 os.unlink(dot_path) 0681 os.unlink(simplified_dot_path) 0682 return True 0683 0684 0685 def create_fw_context(args, lib, tagfiles, copyright=''): 0686 0687 # There is one more level for groups 0688 if lib.part_of_group: 0689 corrected_tagfiles = [] 0690 for k in range(len(tagfiles)): 0691 # tagfiles are tuples like: 0692 # ('/usr/share/doc/qt/KF5Completion.tags', '/usr/share/doc/qt') 0693 # ('/where/the/tagfile/is/Name.tags', '/where/the/root/folder/is') 0694 if tagfiles[k][1].startswith("http://") or tagfiles[k][1].startswith("https://"): 0695 corrected_tagfiles.append(tagfiles[k]) 0696 else: 0697 corrected_tagfiles.append((tagfiles[k][0], '../' + tagfiles[k][1])) 0698 else: 0699 corrected_tagfiles = tagfiles 0700 0701 return Context(args, 0702 # Names 0703 modulename=lib.name, 0704 fancyname=lib.fancyname, 0705 fwinfo=lib, 0706 # KApidox files 0707 resourcedir=('../../../resources' if lib.part_of_group 0708 else '../../resources'), 0709 # Input 0710 copyright=copyright, 0711 tagfiles=corrected_tagfiles, 0712 dependency_diagram=lib.dependency_diagram, 0713 # Output 0714 outputdir=lib.outputdir, 0715 is_qdoc=lib.metainfo['qdoc'], 0716 ) 0717 0718 0719 def gen_fw_apidocs(ctx, tmp_base_dir): 0720 create_dirs(ctx) 0721 # tmp_dir is deleted when tmp_base_dir is 0722 tmp_dir = tempfile.mkdtemp(prefix=ctx.modulename + '-', dir=tmp_base_dir) 0723 generate_apidocs(ctx, tmp_dir, 0724 doxyfile_entries=dict(WARN_IF_UNDOCUMENTED=True) 0725 ) 0726 0727 0728 def create_fw_tagfile_tuple(lib): 0729 tagfile = os.path.abspath( 0730 os.path.join( 0731 lib.outputdir, 0732 'html', 0733 lib.fancyname+'.tags')) 0734 if lib.part_of_group: 0735 prefix = '../../' 0736 else: 0737 prefix = '../../' 0738 return tagfile, prefix + lib.outputdir + '/html/' 0739 0740 0741 def finish_fw_apidocs_doxygen(ctx: Context, env: Dict[str, Any]): 0742 tmpl = create_jinja_environment(ctx.doxdatadir).get_template('library.html') 0743 postprocess_internal(ctx.htmldir, tmpl, env) 0744 0745 tmpl2 = create_jinja_environment(ctx.doxdatadir).get_template('search.html') 0746 search_output = ctx.fwinfo.outputdir + "/html/search.html" 0747 with codecs.open(search_output, 'w', 'utf-8') as outf: 0748 outf.write(tmpl2.render(env)) 0749 0750 0751 def finish_fw_apidocs_qdoc(ctx: Context, env: Dict[str, Any]): 0752 tmpl = create_jinja_environment(ctx.doxdatadir).get_template('qdoc-wrapper.html') 0753 postprocess_internal_qdoc(ctx.htmldir, tmpl, env) 0754 0755 0756 def gen_template_environment(ctx: Context) -> Dict[str, Any]: 0757 classmap = build_classmap(ctx.tagfile) 0758 0759 entries = [{ 0760 'href': '../../index.html', 0761 'text': 'KDE API Reference' 0762 }] 0763 0764 if ctx.fwinfo.part_of_group: 0765 entries[0]['href'] = '../' + entries[0]['href'] 0766 entries.append({'href': '../../index.html', 'text': ctx.fwinfo.product.fancyname }) 0767 0768 entries.append({'href': 'index.html', 'text': ctx.fancyname }) 0769 0770 mapping = { 0771 'qch': ctx.qhp, 0772 'doxygencss': 'doxygen.css', 0773 'resources': ctx.resourcedir, 0774 'title': ctx.title, 0775 'fwinfo': ctx.fwinfo, 0776 'copyright': f"1996-{datetime.date.today().year} The KDE developers", 0777 'doxygen_menu': {'entries': menu_items(ctx.htmldir, ctx.modulename)}, 0778 'class_map': {'classes': classmap}, 0779 'kapidox_version': utils.get_kapidox_version(), 0780 'breadcrumbs': { 0781 'entries': entries 0782 }, 0783 } 0784 0785 return mapping 0786 0787 0788 def finish_fw_apidocs(ctx: Context): 0789 env = gen_template_environment(ctx) 0790 0791 if ctx.is_qdoc: 0792 logging.info('Postprocessing QtDoc...') 0793 0794 finish_fw_apidocs_qdoc(ctx, env) 0795 0796 else: 0797 logging.info('Postprocessing Doxygen...') 0798 0799 finish_fw_apidocs_doxygen(ctx, env) 0800 0801 0802 def indexer(lib): 0803 """ Create json index from xml 0804 <add> 0805 <doc> 0806 <field name="type">source</field> 0807 <field name="name">kcmodule.cpp</field> 0808 <field name="url">kcmodule_8cpp_source.html#l00001</field> 0809 <field name="keywords"></field> 0810 <field name="text"></field> 0811 </doc> 0812 </add> 0813 """ 0814 0815 doclist = [] 0816 tree = xmlET.parse(lib.outputdir + '/searchdata.xml') 0817 for doc_child in tree.getroot(): 0818 field = {} 0819 for child in doc_child: 0820 if child.attrib['name'] == "type": 0821 if child.text == 'source': 0822 field = None 0823 break # We go to next <doc> 0824 field['type'] = child.text 0825 elif child.attrib['name'] == "name": 0826 field['name'] = child.text 0827 elif child.attrib['name'] == "url": 0828 field['url'] = child.text 0829 elif child.attrib['name'] == "keywords": 0830 field['keyword'] = child.text 0831 elif child.attrib['name'] == "text": 0832 field['text'] = "" if child.text is None else child.text 0833 if field is not None: 0834 doclist.append(field) 0835 0836 indexdic = { 0837 'name': lib.name, 0838 'fancyname': lib.fancyname, 0839 'docfields': doclist 0840 } 0841 0842 with open(lib.outputdir + '/html/searchdata.json', 'w') as f: 0843 for chunk in json.JSONEncoder().iterencode(indexdic): 0844 f.write(chunk) 0845 0846 0847 def create_product_index(product): 0848 doclist = [] 0849 for lib in product.libraries: 0850 with open(lib.outputdir+'/html/searchdata.json', 'r') as f: 0851 libindex = json.load(f) 0852 for item in libindex['docfields']: 0853 if lib.part_of_group: 0854 item['url'] = lib.name.lower() + '/html/' + item['url'] 0855 else: 0856 item['url'] = 'html/' + item['url'] 0857 doclist.append(libindex) 0858 0859 indexdic = { 0860 'name': product.name, 0861 'fancyname': product.fancyname, 0862 'libraries': doclist 0863 } 0864 0865 with open(product.outputdir + '/searchdata.json', 'w') as f: 0866 for chunk in json.JSONEncoder().iterencode(indexdic): 0867 f.write(chunk) 0868 0869 0870 def create_global_index(products): 0871 doclist = [] 0872 for product in products: 0873 if product.metainfo['qdoc']: 0874 continue 0875 0876 with open(product.outputdir+'/searchdata.json', 'r') as f: 0877 prodindex = json.load(f) 0878 for proditem in prodindex['libraries']: 0879 for item in proditem['docfields']: 0880 item['url'] = os.path.join(product.name, item['url']) 0881 doclist.append(prodindex) 0882 0883 indexdic = { 0884 'all': doclist 0885 } 0886 with open('searchdata.json', 'w') as f: 0887 for chunk in json.JSONEncoder().iterencode(indexdic): 0888 f.write(chunk) 0889 0890 0891 def create_qch(products, tagfiles): 0892 tag_root = "QtHelpProject" 0893 tag_files = "files" 0894 tag_filter_section = "filterSection" 0895 tag_keywords = "keywords" 0896 tag_toc = "toc" 0897 for product in products: 0898 tree_out = ET.ElementTree(ET.Element(tag_root)) 0899 root_out = tree_out.getroot() 0900 root_out.set("version", "1.0") 0901 namespace = ET.SubElement(root_out, "namespace") 0902 namespace.text = "org.kde." + product.name 0903 virtual_folder = ET.SubElement(root_out, "virtualFolder") 0904 virtual_folder.text = product.name 0905 filter_section = ET.SubElement(root_out, tag_filter_section) 0906 filter_attribute = ET.SubElement(filter_section, "filterAttribute") 0907 filter_attribute.text = "doxygen" 0908 toc = ET.SubElement(filter_section, "toc") 0909 keywords = ET.SubElement(filter_section, tag_keywords) 0910 if len(product.libraries) > 0: 0911 if product.libraries[0].part_of_group: 0912 product_index_section = ET.SubElement(toc, "section", {'ref': product.name + "/index.html", 'title': product.fancyname}) 0913 files = ET.SubElement(filter_section, tag_files) 0914 0915 for lib in sorted(product.libraries, key=lambda lib: lib.name): 0916 tree = ET.parse(lib.outputdir + '/html/index.qhp') 0917 root = tree.getroot() 0918 for child in root.findall(".//*[@ref]"): 0919 if lib.part_of_group: 0920 child.attrib['ref'] = lib.name + "/html/" + child.attrib['ref'] 0921 else: 0922 child.attrib['ref'] = "html/" + child.attrib['ref'] 0923 child.attrib['ref'] = product.name + '/' + child.attrib['ref'] 0924 0925 for child in root.find(".//"+tag_toc): 0926 if lib.part_of_group: 0927 product_index_section.append(child) 0928 else: 0929 toc.append(child) 0930 0931 for child in root.find(".//keywords"): 0932 keywords.append(child) 0933 0934 resources = [ 0935 "*.json", 0936 product.name + "/*.json", 0937 "resources/css/*.css", 0938 "resources/3rd-party/bootstrap/css/*.css", 0939 "resources/3rd-party/jquery/jquery-3.1.0.min.js", 0940 "resources/*.svg", 0941 "resources/js/*.js", 0942 "resources/icons/*", 0943 ] 0944 if product.part_of_group: 0945 resources.extend([ 0946 product.name + "/*.html", 0947 product.name + "/" + lib.name + "/html/*.html", 0948 product.name + "/" + lib.name + "/html/*.png", 0949 product.name + "/" + lib.name + "/html/*.css", 0950 product.name + "/" + lib.name + "/html/*.js", 0951 product.name + "/" + lib.name + "/html/*.json" 0952 ]) 0953 0954 else: 0955 resources.extend([ 0956 product.name + "/html/*.html", 0957 product.name + "/html/*.png", 0958 product.name + "/html/*.css", 0959 product.name + "/html/*.js" 0960 ]) 0961 0962 for resource in resources: 0963 file_elem = ET.SubElement(files, "file") 0964 file_elem.text = resource 0965 0966 if not os.path.isdir('qch'): 0967 os.mkdir('qch') 0968 0969 name = product.name+".qhp" 0970 outname = product.name+".qch" 0971 tree_out.write(name, encoding="utf-8", xml_declaration=True) 0972 0973 # On many distributions, qhelpgenerator from Qt5 is suffixed with 0974 # "-qt5". Look for it first, and fall back to unsuffixed one if 0975 # not found. 0976 qhelpgenerator = find_executable("qhelpgenerator-qt5") 0977 0978 if qhelpgenerator is None: 0979 qhelpgenerator = "qhelpgenerator" 0980 0981 subprocess.call([qhelpgenerator, name, '-o', 'qch/'+outname]) 0982 os.remove(name)