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)