File indexing completed on 2024-04-28 15:18:37

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)