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