KApiDox

generator.py
1 # -*- coding: utf-8 -*-
2 #
3 # SPDX-FileCopyrightText: 2014 Alex Merry <[email protected]>
4 # SPDX-FileCopyrightText: 2014 Aurélien Gâteau <[email protected]>
5 # SPDX-FileCopyrightText: 2014 Alex Turbov <[email protected]>
6 # SPDX-FileCopyrightText: 2016 Olivier Churlaud <[email protected]>
7 #
8 # SPDX-License-Identifier: BSD-2-Clause
9 
10 import codecs
11 from distutils.spawn import find_executable
12 import datetime
13 import os
14 import logging
15 from os import environ
16 import shutil
17 import subprocess
18 import tempfile
19 import sys
20 import pathlib
21 from typing import Any, Dict
22 import xml.etree.ElementTree as ET
23 import re
24 import glob
25 from pathlib import Path
26 
27 import jinja2
28 
29 from urllib.parse import urljoin
30 
31 import xml.etree.ElementTree as xmlET
32 import json
33 
34 from jinja2.environment import Template
35 
36 from kapidox import utils
37 try:
38  from kapidox import depdiagram
39  DEPDIAGRAM_AVAILABLE = True
40 except ImportError:
41  DEPDIAGRAM_AVAILABLE = False
42 
43 from .doxyfilewriter import DoxyfileWriter
44 
45 
46 ## @package kapidox.generator
47 #
48 # The generator
49 
50 __all__ = (
51  "Context",
52  "generate_apidocs",
53  "search_for_tagfiles",
54  "WARN_LOGFILE",
55  "build_classmap",
56  "postprocess",
57  "create_dirs",
58  "create_jinja_environment",
59  )
60 
61 WARN_LOGFILE = 'doxygen-warnings.log'
62 
63 HTML_SUBDIR = 'html'
64 
65 
66 class Context(object):
67  """
68  Holds parameters used by the various functions of the generator
69  """
70  __slots__ = (
71  # Names
72  'modulename',
73  'fancyname',
74  'title',
75  'fwinfo',
76  # KApidox files
77  'doxdatadir',
78  'resourcedir',
79  # Input
80  'srcdir',
81  'tagfiles',
82  'dependency_diagram',
83  'copyright',
84  'is_qdoc',
85  # Output
86  'outputdir',
87  'htmldir',
88  'tagfile',
89  # Output options
90  'man_pages',
91  'qhp',
92  # Binaries
93  'doxygen',
94  'qhelpgenerator',
95  )
96 
97  def __init__(self, args, **kwargs):
98  # Names
99  self.title = args.title
100  # KApidox files
101  self.doxdatadir = args.doxdatadir
102  # Output options
103  self.man_pages = args.man_pages
104  self.qhp = args.qhp
105  # Binaries
106  self.doxygen = args.doxygen
107  self.qhelpgenerator = args.qhelpgenerator
108 
109  for key in self.__slots__:
110  if not hasattr(self, key):
111  setattr(self, key, kwargs.get(key))
112 
113 
114 def create_jinja_environment(doxdatadir):
115  loader = jinja2.FileSystemLoader(os.path.join(doxdatadir, 'templates'))
116  return jinja2.Environment(loader=loader)
117 
118 
119 def process_toplevel_html_file(outputfile, doxdatadir, products, title, qch_enabled=False):
120 
121  def sort_product(product):
122  print(product.fancyname)
123  if product.fancyname == "The KDE Frameworks":
124  return 'aa'
125  if product.fancyname == "KDE PIM":
126  return 'ab'
127  return product.fancyname.lower()
128 
129  products.sort(key=sort_product)
130  mapping = {
131  'resources': './resources',
132  # steal the doxygen css from one of the frameworks
133  # this means that all the doxygen-provided images etc. will be found
134  'title': title,
135  'qch': qch_enabled,
136  'breadcrumbs': {
137  'entries': [
138  {
139  'href': './index.html',
140  'text': 'KDE API Reference'
141  }
142  ]
143  },
144  'product_list': products,
145  }
146  tmpl = create_jinja_environment(doxdatadir).get_template('frontpage.html')
147  with codecs.open(outputfile, 'w', 'utf-8') as outf:
148  outf.write(tmpl.render(mapping))
149 
150  tmpl2 = create_jinja_environment(doxdatadir).get_template('search.html')
151  search_output = "search.html"
152  with codecs.open(search_output, 'w', 'utf-8') as outf:
153  outf.write(tmpl2.render(mapping))
154 
155 
156 def process_subgroup_html_files(outputfile, doxdatadir, groups, available_platforms, title, qch_enabled=False):
157 
158  for group in groups:
159  mapping = {
160  'resources': '../resources',
161  'title': title,
162  'qch': qch_enabled,
163  'breadcrumbs': {
164  'entries': [
165  {
166  'href': '../index.html',
167  'text': 'KDE API Reference'
168  },
169  {
170  'href': './index.html',
171  'text': group.fancyname
172  }
173  ]
174  },
175  'group': group,
176  'available_platforms': sorted(available_platforms),
177  }
178 
179  if not os.path.isdir(group.name):
180  os.mkdir(group.name)
181  outputfile = group.name + '/index.html'
182  tmpl = create_jinja_environment(doxdatadir).get_template('subgroup.html')
183  with codecs.open(outputfile, 'w', 'utf-8') as outf:
184  outf.write(tmpl.render(mapping))
185 
186  tmpl2 = create_jinja_environment(doxdatadir).get_template('search.html')
187  search_output = group.name + "/search.html"
188  with codecs.open(search_output, 'w', 'utf-8') as outf:
189  outf.write(tmpl2.render(mapping))
190 
191 
192 def create_dirs(ctx):
193  ctx.htmldir = os.path.join(ctx.outputdir, HTML_SUBDIR)
194  ctx.tagfile = os.path.join(ctx.htmldir, ctx.fwinfo.fancyname + '.tags')
195 
196  if not os.path.exists(ctx.outputdir):
197  os.makedirs(ctx.outputdir)
198 
199  if os.path.exists(ctx.htmldir):
200  # If we have files left there from a previous run but which are no
201  # longer generated (for example because a C++ class has been removed)
202  # then postprocess will fail because the left-over file has already been
203  # processed. To avoid that, we delete the html dir.
204  shutil.rmtree(ctx.htmldir)
205  os.makedirs(ctx.htmldir)
206 
207 
208 def load_template(path):
209  # Set errors to 'ignore' because we don't want weird characters in Doxygen
210  # output (for example source code listing) to cause a failure
211  content = codecs.open(path, encoding='utf-8', errors='ignore').read()
212  try:
213  return jinja2.Template(content)
214  except jinja2.exceptions.TemplateSyntaxError:
215  logging.error('Failed to parse template {}'.format(path))
216  raise
217 
218 
219 def find_tagfiles(docdir, doclink=None, flattenlinks=False, exclude=None, _depth=0):
220  """Find Doxygen-generated tag files in a directory.
221 
222  The tag files must have the extension .tags, and must be in the listed
223  directory, a subdirectory or a subdirectory named html of a subdirectory.
224 
225  Args:
226  docdir: (string) the directory to search.
227  doclink: (string) the path or URL to use when creating the
228  documentation links; if None, this will default to
229  docdir. (optional, default None)
230  flattenlinks: (bool) if True, generated links will assume all the html
231  files are directly under doclink; else the html files are
232  assumed to be at the same relative location to doclink as
233  the tag file is to docdir; ignored if doclink is not set.
234  (optional, default False)
235 
236  Returns:
237  A list of pairs of (tag_file,link_path).
238  """
239 
240  if not os.path.isdir(docdir):
241  return []
242 
243  if doclink is None:
244  doclink = docdir
245  flattenlinks = False
246 
247  def smartjoin(pathorurl1, *args):
248  """Join paths or URLS
249 
250  It figures out which it is from whether the first contains a "://"
251  """
252  if '://' in pathorurl1:
253  if not pathorurl1.endswith('/'):
254  pathorurl1 += '/'
255  return urljoin(pathorurl1, *args)
256  else:
257  return os.path.join(pathorurl1, *args)
258 
259  def nestedlink(subdir):
260  if flattenlinks:
261  return doclink
262  else:
263  return smartjoin(doclink, subdir)
264 
265  tagfiles = []
266 
267  entries = os.listdir(docdir)
268  for e in entries:
269  if e == exclude:
270  continue
271  path = os.path.join(docdir, e)
272  if os.path.isfile(path) and e.endswith('.tags'):
273  tagfiles.append((path, doclink))
274  elif (_depth == 0 or (_depth == 1 and e == 'html')) and os.path.isdir(path):
275  tagfiles += find_tagfiles(path, nestedlink(e),
276  flattenlinks=flattenlinks,
277  _depth=_depth+1,
278  exclude=exclude)
279 
280  return tagfiles
281 
282 
283 def search_for_tagfiles(suggestion=None, doclink=None, flattenlinks=False, searchpaths=[], exclude=None):
284  """Find Doxygen-generated tag files
285 
286  See the find_tagfiles documentation for how the search is carried out in
287  each directory; this just allows a list of directories to be searched.
288 
289  At least one of docdir or searchpaths must be given for it to find anything.
290 
291  Args:
292  suggestion: the first place to look (will complain if there are no
293  documentation tag files there)
294  doclink: the path or URL to use when creating the documentation
295  links; if None, this will default to docdir
296  flattenlinks: if this is True, generated links will assume all the html
297  files are directly under doclink; if False (the default),
298  the html files are assumed to be at the same relative
299  location to doclink as the tag file is to docdir; ignored
300  if doclink is not set
301  searchpaths: other places to look for documentation tag files
302 
303  Returns:
304  A list of pairs of (tag_file,link_path)
305  """
306 
307  if not suggestion is None:
308  if not os.path.isdir(suggestion):
309  logging.warning(suggestion + " is not a directory")
310  else:
311  tagfiles = find_tagfiles(suggestion, doclink, flattenlinks, exclude)
312  if len(tagfiles) == 0:
313  logging.warning(suggestion + " does not contain any tag files")
314  else:
315  return tagfiles
316 
317  for d in searchpaths:
318  tagfiles = find_tagfiles(d, doclink, flattenlinks, exclude)
319  if len(tagfiles) > 0:
320  logging.info("Documentation tag files found at " + d)
321  return tagfiles
322 
323  return []
324 
325 
326 
327 def menu_items(htmldir, modulename):
328  """Menu items for standard Doxygen files
329 
330  Looks for a set of standard Doxygen files (like namespaces.html) and
331  provides menu text for those it finds in htmldir.
332 
333  Args:
334  htmldir: (string) the directory the HTML files are contained in.
335  modulname: (string) the name of the library
336 
337  Returns:
338  A list of maps with 'text' and 'href' keys.
339  """
340  entries = [
341  {'text': 'Main Page', 'href': 'index.html'},
342  {'text': 'Namespace List', 'href': 'namespaces.html'},
343  {'text': 'Namespace Members', 'href': 'namespacemembers.html'},
344  {'text': 'Alphabetical List', 'href': 'classes.html'},
345  {'text': 'Class List', 'href': 'annotated.html'},
346  {'text': 'Class Hierarchy', 'href': 'hierarchy.html'},
347  {'text': 'File List', 'href': 'files.html'},
348  {'text': 'File Members', 'href': 'globals.html'},
349  {'text': 'Modules', 'href': 'modules.html'},
350  {'text': 'Directories', 'href': 'dirs.html'},
351  {'text': 'Dependencies', 'href': modulename + '-dependencies.html'},
352  {'text': 'Related Pages', 'href': 'pages.html'},
353  ]
354  # NOTE In Python 3 filter() builtin returns an iterable, but not a list
355  # type, so explicit conversion is here!
356  return list(filter(
357  lambda e: os.path.isfile(os.path.join(htmldir, e['href'])),
358  entries))
359 
360 
361 def parse_dox_html(stream):
362  """Parse the HTML files produced by Doxygen, extract the key/value block we
363  add through header.html and return a dict ready for the Jinja template.
364 
365  The HTML files produced by Doxygen with our custom header and footer files
366  look like this:
367 
368  @code
369  <!--
370  key1: value1
371  key2: value2
372  ...
373  -->
374  <html>
375  <head>
376  ...
377  </head>
378  <body>
379  ...
380  </body>
381  </html>
382  @endcode
383 
384  The parser fills the dict from the top key/value block, and add the content
385  of the body to the dict using the "content" key.
386 
387  We do not use an XML parser because the HTML file might not be well-formed,
388  for example if the documentation contains raw HTML.
389 
390  The key/value block is kept in a comment so that it does not appear in Qt
391  Compressed Help output, which is not postprocessed by ourself.
392  """
393  dct = {}
394  body = []
395 
396  def parse_key_value_block(line):
397  if line == "<!--":
398  return parse_key_value_block
399  if line == "-->":
400  return skip_head
401  key, value = line.split(': ', 1)
402  dct[key] = value
403  return parse_key_value_block
404 
405  def skip_head(line):
406  if line == "<body>":
407  return extract_body
408  else:
409  return skip_head
410 
411  def extract_body(line):
412  if line == "</body>":
413  return None
414  body.append(line)
415  return extract_body
416 
417  parser = parse_key_value_block
418  while parser is not None:
419  line = stream.readline().rstrip()
420  parser = parser(line)
421 
422  dct['content'] = '\n'.join(body)
423  return dct
424 
425 def postprocess_internal_qdoc(htmldir: str, tmpl: Template, env: Dict[str, Any]):
426  """Substitute text in HTML files
427 
428  Performs text substitutions on each line in each .html file in a directory.
429 
430  Args:
431  htmldir: (string) the directory containing the .html files.
432  mapping: (dict) a dict of mappings.
433 
434  """
435  for path in glob.glob(os.path.join(htmldir, "*.html")):
436  newpath = f"{path}.new"
437 
438  txt = Path(path).read_text()
439  env['docs'] = txt.partition('body')[2].partition('</body>')[0]
440 
441  with codecs.open(newpath, 'w', 'utf-8') as outf:
442  try:
443  html = tmpl.render(env)
444  except BaseException:
445  logging.error(f"Postprocessing {path} failed")
446  raise
447 
448  outf.write(html)
449 
450  os.remove(path)
451  os.rename(newpath, path)
452 
453 
454 def postprocess_internal(htmldir, tmpl, mapping):
455  """Substitute text in HTML files
456 
457  Performs text substitutions on each line in each .html file in a directory.
458 
459  Args:
460  htmldir: (string) the directory containing the .html files.
461  mapping: (dict) a dict of mappings.
462 
463  """
464  for name in os.listdir(htmldir):
465  if name.endswith('.html'):
466  path = os.path.join(htmldir, name)
467  newpath = path + '.new'
468 
469  if name != 'classes.html' and name.startswith('class'):
470  mapping['classname'] = name[5:-5].split('_1_1')[-1]
471  mapping['fullname'] = name[5:-5].replace('_1_1', '::')
472  elif name.startswith('namespace') and name != 'namespaces.html' and not name.startswith('namespacemembers'):
473  mapping['classname'] = None
474  mapping['fullname'] = name[9:-5].replace('_1_1', '::')
475  else:
476  mapping['classname'] = None
477  mapping['fullname'] = None
478 
479  with codecs.open(path, 'r', 'utf-8', errors='ignore') as f:
480  mapping['dox'] = parse_dox_html(f)
481 
482  with codecs.open(newpath, 'w', 'utf-8') as outf:
483  try:
484  html = tmpl.render(mapping)
485  except Exception:
486  logging.error('postprocessing {} failed'.format(path))
487  raise
488  outf.write(html)
489  os.remove(path)
490  os.rename(newpath, path)
491 
492 
493 def build_classmap(tagfile):
494  """Parses a tagfile to get a map from classes to files
495 
496  Args:
497  tagfile: the Doxygen-generated tagfile to parse.
498 
499  Returns:
500  A list of maps (keys: classname and filename).
501  """
502  import xml.etree.ElementTree as ET
503  tree = ET.parse(tagfile)
504  tagfile_root = tree.getroot()
505  mapping = []
506  for compound in tagfile_root:
507  kind = compound.get('kind')
508  if kind == 'class' or kind == 'namespace':
509  name_el = compound.find('name')
510  filename_el = compound.find('filename')
511  mapping.append({'classname': name_el.text,
512  'filename': filename_el.text})
513  return mapping
514 
515 
516 def generate_dependencies_page(tmp_dir, doxdatadir, modulename, dependency_diagram):
517  """Create `modulename`-dependencies.md in `tmp_dir`"""
518  template_path = os.path.join(doxdatadir, 'dependencies.md.tmpl')
519  out_path = os.path.join(tmp_dir, modulename + '-dependencies.md')
520  tmpl = load_template(template_path)
521  with codecs.open(out_path, 'w', 'utf-8') as outf:
522  txt = tmpl.render({
523  'modulename': modulename,
524  'diagramname': os.path.basename(dependency_diagram),
525  })
526  outf.write(txt)
527  return out_path
528 
529 def generate_apidocs_qdoc(ctx: Context, tmp_dir: str, doxyfile_entries=None, keep_temp_dirs=False):
530  absolute = pathlib.Path(os.path.join(ctx.outputdir, 'html')).absolute()
531 
532  environ['KAPIDOX_DIR'] = ctx.doxdatadir
533 
534  logging.info(f'Running QDoc (qdoc {ctx.fwinfo.path}/.qdocconf --outputdir={absolute}')
535  ret = subprocess.call(['qdoc', ctx.fwinfo.path + "/.qdocconf", f"--outputdir={absolute}"])
536  if ret != 0:
537  raise Exception("QDoc exited with a non-zero status code")
538 
539 def generate_apidocs(ctx: Context, tmp_dir, doxyfile_entries=None, keep_temp_dirs=False):
540  """Generate the API documentation for a single directory"""
541 
542  if ctx.is_qdoc:
543  return generate_apidocs_qdoc(ctx, tmp_dir, doxyfile_entries, keep_temp_dirs)
544 
545  def find_src_subdir(dirlist, deeper_subd=None):
546  returnlist = []
547  for d in dirlist:
548  pth = os.path.join(ctx.fwinfo.path, d)
549  if deeper_subd is not None:
550  pth = os.path.join(pth, deeper_subd)
551  if os.path.isdir(pth) or os.path.isfile(pth):
552  returnlist.append(pth)
553  else:
554  pass # We drop it
555  return returnlist
556 
557  input_list = []
558  if os.path.isfile(ctx.fwinfo.path + "/Mainpage.dox"):
559  input_list.append(ctx.fwinfo.path + "/Mainpage.dox")
560  elif os.path.isfile(ctx.fwinfo.path + "/README.md"):
561  input_list.append(ctx.fwinfo.path + "/README.md")
562 
563  input_list.extend(find_src_subdir(ctx.fwinfo.srcdirs))
564  input_list.extend(find_src_subdir(ctx.fwinfo.docdir))
565  image_path_list = []
566 
567  if ctx.dependency_diagram:
568  input_list.append(generate_dependencies_page(tmp_dir,
569  ctx.doxdatadir,
570  ctx.modulename,
571  ctx.dependency_diagram))
572  image_path_list.append(ctx.dependency_diagram)
573 
574  doxyfile_path = os.path.join(tmp_dir, 'Doxyfile')
575  with codecs.open(doxyfile_path, 'w', 'utf-8') as doxyfile:
576  # Global defaults
577  with codecs.open(os.path.join(ctx.doxdatadir, 'Doxyfile.global'),
578  'r', 'utf-8') as f:
579  for line in f:
580  doxyfile.write(line)
581 
582  writer = DoxyfileWriter(doxyfile)
583  writer.write_entry('PROJECT_NAME', ctx.fancyname)
584  # FIXME: can we get the project version from CMake? No from GIT TAGS!
585 
586  # Input locations
587  image_path_list.extend(find_src_subdir(ctx.fwinfo.docdir, 'pics'))
588  writer.write_entries(
589  INPUT=input_list,
590  DOTFILE_DIRS=find_src_subdir(ctx.fwinfo.docdir, 'dot'),
591  EXAMPLE_PATH=find_src_subdir(ctx.fwinfo.exampledirs),
592  IMAGE_PATH=image_path_list)
593 
594  # Other input settings
595  writer.write_entry('TAGFILES', [f + '=' + loc for f, loc in ctx.tagfiles])
596 
597  # Output locations
598  writer.write_entries(
599  OUTPUT_DIRECTORY=ctx.outputdir,
600  GENERATE_TAGFILE=ctx.tagfile,
601  HTML_OUTPUT=HTML_SUBDIR,
602  WARN_LOGFILE=os.path.join(ctx.outputdir, WARN_LOGFILE))
603 
604  # Other output settings
605  writer.write_entries(
606  HTML_HEADER=ctx.doxdatadir + '/header.html',
607  HTML_FOOTER=ctx.doxdatadir + '/footer.html'
608  )
609 
610  # Set a layout so that properties are first
611  writer.write_entries(
612  LAYOUT_FILE=ctx.doxdatadir + '/DoxygenLayout.xml'
613  )
614 
615  # Always write these, even if QHP is disabled, in case Doxygen.local
616  # overrides it
617  writer.write_entries(
618  QHP_VIRTUAL_FOLDER=ctx.modulename,
619  QHP_NAMESPACE="org.kde." + ctx.modulename,
620  QHG_LOCATION=ctx.qhelpgenerator)
621 
622  writer.write_entries(
623  GENERATE_MAN=ctx.man_pages,
624  GENERATE_QHP=ctx.qhp)
625 
626  if doxyfile_entries:
627  writer.write_entries(**doxyfile_entries)
628 
629  # Module-specific overrides
630  if find_src_subdir(ctx.fwinfo.docdir):
631  localdoxyfile = os.path.join(find_src_subdir(ctx.fwinfo.docdir)[0], 'Doxyfile.local')
632  if os.path.isfile(localdoxyfile):
633  with codecs.open(localdoxyfile, 'r', 'utf-8') as f:
634  for line in f:
635  doxyfile.write(line)
636 
637  logging.info('Running Doxygen')
638  subprocess.call([ctx.doxygen, doxyfile_path])
639 
640 def generate_diagram(png_path, fancyname, dot_files, tmp_dir):
641  """Generate a dependency diagram for a framework.
642  """
643  def run_cmd(cmd, **kwargs):
644  try:
645  subprocess.check_call(cmd, **kwargs)
646  except subprocess.CalledProcessError as exc:
647  logging.error('Command {exc.cmd} failed with error code {}.'
648  .format(exc.returncode))
649  return False
650  return True
651 
652  logging.info('Generating dependency diagram')
653  dot_path = os.path.join(tmp_dir, fancyname + '.dot')
654 
655  with open(dot_path, 'w') as f:
656  with_qt = False
657  ok = depdiagram.generate(f, dot_files, framework=fancyname,
658  with_qt=with_qt)
659  if not ok:
660  logging.error('Generating diagram failed')
661  return False
662 
663  logging.info('- Simplifying diagram')
664  simplified_dot_path = os.path.join(tmp_dir, fancyname + '-simplified.dot')
665  with open(simplified_dot_path, 'w') as f:
666  if not run_cmd(['tred', dot_path], stdout=f):
667  return False
668 
669  logging.info('- Generating diagram png')
670  if not run_cmd(['dot', '-Tpng', '-o' + png_path, simplified_dot_path]):
671  return False
672 
673  # These os.unlink() calls are not in a 'finally' block on purpose.
674  # Keeping the dot files around makes it possible to inspect their content
675  # when running with the --keep-temp-dirs option. If generation fails and
676  # --keep-temp-dirs is not set, the files will be removed when the program
677  # ends because they were created in `tmp_dir`.
678  os.unlink(dot_path)
679  os.unlink(simplified_dot_path)
680  return True
681 
682 
683 def create_fw_context(args, lib, tagfiles, copyright=''):
684 
685  # There is one more level for groups
686  if lib.part_of_group:
687  corrected_tagfiles = []
688  for k in range(len(tagfiles)):
689  # tagfiles are tuples like:
690  # ('/usr/share/doc/qt/KF5Completion.tags', '/usr/share/doc/qt')
691  # ('/where/the/tagfile/is/Name.tags', '/where/the/root/folder/is')
692  if tagfiles[k][1].startswith("http://") or tagfiles[k][1].startswith("https://"):
693  corrected_tagfiles.append(tagfiles[k])
694  else:
695  corrected_tagfiles.append((tagfiles[k][0], '../' + tagfiles[k][1]))
696  else:
697  corrected_tagfiles = tagfiles
698 
699  return Context(args,
700  # Names
701  modulename=lib.name,
702  fancyname=lib.fancyname,
703  fwinfo=lib,
704  # KApidox files
705  resourcedir=('../../../resources' if lib.part_of_group
706  else '../../resources'),
707  # Input
708  copyright=copyright,
709  tagfiles=corrected_tagfiles,
710  dependency_diagram=lib.dependency_diagram,
711  # Output
712  outputdir=lib.outputdir,
713  is_qdoc=lib.metainfo['qdoc'],
714  )
715 
716 
717 def gen_fw_apidocs(ctx, tmp_base_dir):
718  create_dirs(ctx)
719  # tmp_dir is deleted when tmp_base_dir is
720  tmp_dir = tempfile.mkdtemp(prefix=ctx.modulename + '-', dir=tmp_base_dir)
721  generate_apidocs(ctx, tmp_dir,
722  doxyfile_entries=dict(WARN_IF_UNDOCUMENTED=True)
723  )
724 
725 def create_fw_tagfile_tuple(lib):
726  tagfile = os.path.abspath(
727  os.path.join(
728  lib.outputdir,
729  'html',
730  lib.fancyname+'.tags'))
731  if lib.part_of_group:
732  prefix = '../../'
733  else:
734  prefix = '../../'
735  return (tagfile, prefix + lib.outputdir + '/html/')
736 
737 def finish_fw_apidocs_doxygen(ctx: Context, env: Dict[str, Any]):
738  tmpl = create_jinja_environment(ctx.doxdatadir).get_template('library.html')
739  postprocess_internal(ctx.htmldir, tmpl, env)
740 
741  tmpl2 = create_jinja_environment(ctx.doxdatadir).get_template('search.html')
742  search_output = ctx.fwinfo.outputdir + "/html/search.html"
743  with codecs.open(search_output, 'w', 'utf-8') as outf:
744  outf.write(tmpl2.render(env))
745 
746 def finish_fw_apidocs_qdoc(ctx: Context, env: Dict[str, Any]):
747  tmpl = create_jinja_environment(ctx.doxdatadir).get_template('qdoc-wrapper.html')
748  postprocess_internal_qdoc(ctx.htmldir, tmpl, env)
749 
750 def gen_template_environment(ctx: Context) -> Dict[str, Any]:
751  classmap = build_classmap(ctx.tagfile)
752 
753  entries = [{
754  'href': '../../index.html',
755  'text': 'KDE API Reference'
756  }]
757 
758  if ctx.fwinfo.part_of_group:
759  entries[0]['href'] = '../' + entries[0]['href']
760  entries.append({'href': '../../index.html', 'text': ctx.fwinfo.product.fancyname })
761 
762  entries.append({'href': 'index.html', 'text': ctx.fancyname })
763 
764  mapping = {
765  'qch': ctx.qhp,
766  'doxygencss': 'doxygen.css',
767  'resources': ctx.resourcedir,
768  'title': ctx.title,
769  'fwinfo': ctx.fwinfo,
770  'copyright': f"1996-{datetime.date.today().year} The KDE developers",
771  'doxygen_menu': {'entries': menu_items(ctx.htmldir, ctx.modulename)},
772  'class_map': {'classes': classmap},
773  'kapidox_version': utils.get_kapidox_version(),
774  'breadcrumbs': {
775  'entries': entries
776  },
777  }
778 
779  return mapping
780 
781 def finish_fw_apidocs(ctx: Context):
782  env = gen_template_environment(ctx)
783 
784  if ctx.is_qdoc:
785  logging.info('Postprocessing QtDoc...')
786 
787  finish_fw_apidocs_qdoc(ctx, env)
788 
789  else:
790  logging.info('Postprocessing Doxygen...')
791 
792  finish_fw_apidocs_doxygen(ctx, env)
793 
794 def indexer(lib):
795  """ Create json index from xml
796  <add>
797  <doc>
798  <field name="type">source</field>
799  <field name="name">kcmodule.cpp</field>
800  <field name="url">kcmodule_8cpp_source.html#l00001</field>
801  <field name="keywords"></field>
802  <field name="text"></field>
803  </doc>
804  </add
805  """
806 
807  doclist = []
808  tree = xmlET.parse(lib.outputdir + '/searchdata.xml')
809  for doc_child in tree.getroot():
810  field = {}
811  for child in doc_child:
812  if child.attrib['name'] == "type":
813  if child.text == 'source':
814  field = None
815  break; # We go to next <doc>
816  field['type'] = child.text
817  elif child.attrib['name'] == "name":
818  field['name'] = child.text
819  elif child.attrib['name'] == "url":
820  field['url'] = child.text
821  elif child.attrib['name'] == "keywords":
822  field['keyword'] = child.text
823  elif child.attrib['name'] == "text":
824  field['text'] = "" if child.text is None else child.text
825  if field is not None:
826  doclist.append(field)
827 
828  indexdic = {
829  'name': lib.name,
830  'fancyname': lib.fancyname,
831  'docfields': doclist
832  }
833 
834  with open(lib.outputdir + '/html/searchdata.json', 'w') as f:
835  for chunk in json.JSONEncoder().iterencode(indexdic):
836  f.write(chunk)
837 
838 def create_product_index(product):
839  doclist = []
840  for lib in product.libraries:
841  with open(lib.outputdir+'/html/searchdata.json', 'r') as f:
842  libindex = json.load(f)
843  for item in libindex['docfields']:
844  if lib.part_of_group:
845  item['url'] = lib.name + '/html/' + item['url']
846  else:
847  item['url'] = 'html/' + item['url']
848  doclist.append(libindex)
849 
850  indexdic = {
851  'name': product.name,
852  'fancyname': product.fancyname,
853  'libraries': doclist
854  }
855 
856  with open(product.outputdir + '/searchdata.json', 'w') as f:
857  for chunk in json.JSONEncoder().iterencode(indexdic):
858  f.write(chunk)
859 
860 def create_global_index(products):
861  doclist = []
862  for product in products:
863  if product.metainfo['qdoc']:
864  continue
865 
866  with open(product.outputdir+'/searchdata.json', 'r') as f:
867  prodindex = json.load(f)
868  for proditem in prodindex['libraries']:
869  for item in proditem['docfields']:
870  item['url'] = product.name + '/' + item['url']
871  doclist.append(prodindex)
872 
873  indexdic = {
874  'all': doclist
875  }
876  with open('searchdata.json', 'w') as f:
877  for chunk in json.JSONEncoder().iterencode(indexdic):
878  f.write(chunk)
879 
880 def create_qch(products, tagfiles):
881  tag_root = "QtHelpProject"
882  tag_files = "files"
883  tag_filterSection = "filterSection"
884  tag_keywords = "keywords"
885  tag_toc = "toc"
886  for product in products:
887  tree_out = ET.ElementTree(ET.Element(tag_root))
888  root_out = tree_out.getroot()
889  root_out.set("version", "1.0")
890  namespace = ET.SubElement(root_out, "namespace")
891  namespace.text = "org.kde." + product.name
892  virtualFolder = ET.SubElement(root_out, "virtualFolder")
893  virtualFolder.text = product.name
894  filterSection = ET.SubElement(root_out, tag_filterSection)
895  filterAttribute = ET.SubElement(filterSection, "filterAttribute")
896  filterAttribute.text = "doxygen"
897  toc = ET.SubElement(filterSection, "toc")
898  keywords = ET.SubElement(filterSection, tag_keywords)
899  if len(product.libraries) > 0:
900  if product.libraries[0].part_of_group:
901  product_indexSection = ET.SubElement(toc, "section", {'ref': product.name + "/index.html", 'title': product.fancyname})
902  files = ET.SubElement(filterSection, tag_files)
903 
904  for lib in sorted(product.libraries, key=lambda lib: lib.name):
905  tree = ET.parse(lib.outputdir + '/html/index.qhp')
906  root = tree.getroot()
907  for child in root.findall(".//*[@ref]"):
908  if lib.part_of_group:
909  child.attrib['ref'] = lib.name + "/html/" + child.attrib['ref']
910  else:
911  child.attrib['ref'] = "html/" + child.attrib['ref']
912  child.attrib['ref'] = product.name + '/' +child.attrib['ref']
913 
914  for child in root.find(".//"+tag_toc):
915  if lib.part_of_group:
916  product_indexSection.append(child)
917  else:
918  toc.append(child)
919 
920  for child in root.find(".//keywords"):
921  keywords.append(child)
922 
923  resources = [
924  "*.json",
925  product.name + "/*.json",
926  "resources/css/*.css",
927  "resources/3rd-party/bootstrap/css/*.css",
928  "resources/3rd-party/jquery/jquery-3.1.0.min.js",
929  "resources/*.svg",
930  "resources/js/*.js",
931  "resources/icons/*",
932  ]
933  if product.part_of_group:
934  resources.extend([
935  product.name + "/*.html",
936  product.name + "/" + lib.name +"/html/*.html",
937  product.name + "/" + lib.name +"/html/*.png",
938  product.name + "/" + lib.name +"/html/*.css",
939  product.name + "/" + lib.name +"/html/*.js",
940  product.name + "/" + lib.name +"/html/*.json"
941  ])
942 
943  else:
944  resources.extend([
945  product.name + "/html/*.html",
946  product.name + "/html/*.png",
947  product.name + "/html/*.css",
948  product.name + "/html/*.js"
949  ])
950 
951  for resource in resources:
952  file_elem = ET.SubElement(files, "file")
953  file_elem.text = resource
954 
955  if not os.path.isdir('qch'):
956  os.mkdir('qch')
957 
958  name = product.name+".qhp"
959  outname = product.name+".qch"
960  tree_out.write(name, encoding="utf-8", xml_declaration=True)
961 
962  # On many distributions, qhelpgenerator from Qt5 is suffixed with
963  # "-qt5". Look for it first, and fall back to unsuffixed one if
964  # not found.
965  qhelpgenerator = find_executable("qhelpgenerator-qt5")
966 
967  if qhelpgenerator is None:
968  qhelpgenerator = "qhelpgenerator"
969 
970  subprocess.call([qhelpgenerator, name, '-o', 'qch/'+outname])
971  os.remove(name)
def search_for_tagfiles(suggestion=None, doclink=None, flattenlinks=False, searchpaths=[], exclude=None)
Find Doxygen-generated tag files.
Definition: generator.py:305
def generate_dependencies_page(tmp_dir, doxdatadir, modulename, dependency_diagram)
Create modulename-dependencies.md in tmp_dir
Definition: generator.py:517
def generate_diagram(png_path, fancyname, dot_files, tmp_dir)
Generate a dependency diagram for a framework.
Definition: generator.py:642
def build_classmap(tagfile)
Parses a tagfile to get a map from classes to files.
Definition: generator.py:501
def indexer(lib)
Create json index from xml <add> <doc> <field name="type">source</field> <field name="name">kcmodule...
Definition: generator.py:805
def postprocess_internal(htmldir, tmpl, mapping)
Substitute text in HTML files.
Definition: generator.py:463
def generate_apidocs
Generate the API documentation for a single directory.
Definition: generator.py:540
def postprocess_internal_qdoc
Substitute text in HTML files.
Definition: generator.py:434
def find_tagfiles(docdir, doclink=None, flattenlinks=False, exclude=None, _depth=0)
Find Doxygen-generated tag files in a directory.
Definition: generator.py:238
Holds parameters used by the various functions of the generator.
Definition: generator.py:69
def menu_items(htmldir, modulename)
Menu items for standard Doxygen files.
Definition: generator.py:339
def parse_dox_html(stream)
Parse the HTML files produced by Doxygen, extract the key/value block we add through header...
Definition: generator.py:392
This file is part of the KDE documentation.
Documentation copyright © 1996-2021 The KDE developers.
Generated on Tue Oct 26 2021 22:55:54 by doxygen 1.8.11 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.