File indexing completed on 2024-07-21 06:35:09

0001 # SPDX-FileCopyrightText: 2014 Alex Merry <alex.merry@kde.org>
0002 #
0003 # Based on cmake.py from CMake:
0004 # SPDX-FileCopyrightText: 2000-2013 Kitware Inc., Insight Software Consortium
0005 #
0006 # SPDX-License-Identifier: BSD-3-Clause
0007 
0008 import html
0009 import os
0010 import re
0011 
0012 # Monkey patch for pygments reporting an error when generator expressions are
0013 # used.
0014 # https://bitbucket.org/birkenfeld/pygments-main/issue/942/cmake-generator-expressions-not-handled
0015 from pygments.lexers import CMakeLexer
0016 from pygments.token import Name, Operator
0017 from pygments.lexer import bygroups
0018 CMakeLexer.tokens["args"].append(('(\\$<)(.+?)(>)',
0019                                   bygroups(Operator, Name.Variable, Operator)))
0020 
0021 # Monkey patch for sphinx generating invalid content for qcollectiongenerator
0022 # https://bitbucket.org/birkenfeld/sphinx/issue/1435/qthelp-builder-should-htmlescape-keywords
0023 try:
0024   from sphinxcontrib.qthelp import QtHelpBuilder
0025 except ImportError:
0026   # sphinx < 4.0
0027   from sphinx.builders.qthelp import QtHelpBuilder
0028 old_build_keywords = QtHelpBuilder.build_keywords
0029 def new_build_keywords(self, title, refs, subitems):
0030   old_items = old_build_keywords(self, title, refs, subitems)
0031   new_items = []
0032   for item in old_items:
0033     before, rest = item.split("ref=\"", 1)
0034     ref, after = rest.split("\"")
0035     if ("<" in ref and ">" in ref):
0036       new_items.append(before + "ref=\"" + html.escape(ref) + "\"" + after)
0037     else:
0038       new_items.append(item)
0039   return new_items
0040 QtHelpBuilder.build_keywords = new_build_keywords
0041 
0042 from docutils.parsers.rst import Directive, directives
0043 from docutils.transforms import Transform
0044 try:
0045     from docutils.utils.error_reporting import SafeString, ErrorString
0046 except ImportError:
0047     # error_reporting was not in utils before version 0.11:
0048     from docutils.error_reporting import SafeString, ErrorString
0049 
0050 from docutils import io, nodes
0051 
0052 from sphinx.directives import ObjectDescription
0053 from sphinx.domains import Domain, ObjType
0054 from sphinx.roles import XRefRole
0055 from sphinx.util.nodes import make_refnode
0056 from sphinx import addnodes
0057 
0058 class ECMModule(Directive):
0059     required_arguments = 1
0060     optional_arguments = 0
0061     final_argument_whitespace = True
0062     option_spec = {'encoding': directives.encoding}
0063 
0064     def __init__(self, *args, **keys):
0065         self.re_start = re.compile(r'^#\[(?P<eq>=*)\[\.rst:$')
0066         Directive.__init__(self, *args, **keys)
0067 
0068     def run(self):
0069         settings = self.state.document.settings
0070         if not settings.file_insertion_enabled:
0071             raise self.warning('"%s" directive disabled.' % self.name)
0072 
0073         env = self.state.document.settings.env
0074         rel_path, path = env.relfn2path(self.arguments[0])
0075         path = os.path.normpath(path)
0076         encoding = self.options.get('encoding', settings.input_encoding)
0077         e_handler = settings.input_encoding_error_handler
0078         try:
0079             settings.record_dependencies.add(path)
0080             f = io.FileInput(source_path=path, encoding=encoding,
0081                              error_handler=e_handler)
0082         except UnicodeEncodeError:
0083             raise self.severe('Problems with "%s" directive path:\n'
0084                               'Cannot encode input file path "%s" '
0085                               '(wrong locale?).' %
0086                               (self.name, SafeString(path)))
0087         except IOError as error:
0088             raise self.severe('Problems with "%s" directive path:\n%s.' %
0089                       (self.name, ErrorString(error)))
0090         raw_lines = f.read().splitlines()
0091         f.close()
0092         rst = None
0093         lines = []
0094         for line in raw_lines:
0095             if rst is not None and rst != '#':
0096                 # Bracket mode: check for end bracket
0097                 pos = line.find(rst)
0098                 if pos >= 0:
0099                     if line[0] == '#':
0100                         line = ''
0101                     else:
0102                         line = line[0:pos]
0103                     rst = None
0104             else:
0105                 # Line mode: check for .rst start (bracket or line)
0106                 m = self.re_start.match(line)
0107                 if m:
0108                     rst = ']%s]' % m.group('eq')
0109                     line = ''
0110                 elif line == '#.rst:':
0111                     rst = '#'
0112                     line = ''
0113                 elif rst == '#':
0114                     if line == '#' or line[:2] == '# ':
0115                         line = line[2:]
0116                     else:
0117                         rst = None
0118                         line = ''
0119                 elif rst is None:
0120                     line = ''
0121             lines.append(line)
0122         if rst is not None and rst != '#':
0123             raise self.warning('"%s" found unclosed bracket "#[%s[.rst:" in %s' %
0124                                (self.name, rst[1:-1], path))
0125         self.state_machine.insert_input(lines, path)
0126         return []
0127 
0128 class _ecm_index_entry:
0129     def __init__(self, desc):
0130         self.desc = desc
0131 
0132     def __call__(self, title, targetid):
0133         return ('pair', u'%s ; %s' % (self.desc, title), targetid, 'main', None)
0134 
0135 _ecm_index_objs = {
0136     'manual':      _ecm_index_entry('manual'),
0137     'module':      _ecm_index_entry('module'),
0138     'find-module': _ecm_index_entry('find-module'),
0139     'kde-module':  _ecm_index_entry('kde-module'),
0140     'toolchain':   _ecm_index_entry('toolchain'),
0141     }
0142 
0143 def _ecm_object_inventory(env, document, line, objtype, targetid):
0144     inv = env.domaindata['ecm']['objects']
0145     if targetid in inv:
0146         document.reporter.warning(
0147             'ECM object "%s" also described in "%s".' %
0148             (targetid, env.doc2path(inv[targetid][0])), line=line)
0149     inv[targetid] = (env.docname, objtype)
0150 
0151 class ECMTransform(Transform):
0152 
0153     # Run this transform early since we insert nodes we want
0154     # treated as if they were written in the documents.
0155     default_priority = 210
0156 
0157     def __init__(self, document, startnode):
0158         Transform.__init__(self, document, startnode)
0159         self.titles = {}
0160 
0161     def parse_title(self, docname):
0162         """Parse a document title as the first line starting in [A-Za-z0-9<]
0163            or fall back to the document basename if no such line exists.
0164            Return the title or False if the document file does not exist.
0165         """
0166         env = self.document.settings.env
0167         title = self.titles.get(docname)
0168         if title is None:
0169             fname = os.path.join(env.srcdir, docname+'.rst')
0170             try:
0171                 f = open(fname, 'r')
0172             except IOError:
0173                 title = False
0174             else:
0175                 for line in f:
0176                     if len(line) > 0 and (line[0].isalnum() or line[0] == '<'):
0177                         title = line.rstrip()
0178                         break
0179                 f.close()
0180                 if title is None:
0181                     title = os.path.basename(docname)
0182             self.titles[docname] = title
0183         return title
0184 
0185     def apply(self):
0186         env = self.document.settings.env
0187 
0188         # Treat some documents as ecm domain objects.
0189         objtype, sep, tail = env.docname.rpartition('/')
0190         make_index_entry = _ecm_index_objs.get(objtype)
0191         if make_index_entry:
0192             title = self.parse_title(env.docname)
0193             # Insert the object link target.
0194             targetid = '%s:%s' % (objtype, title)
0195             targetnode = nodes.target('', '', ids=[targetid])
0196             self.document.insert(0, targetnode)
0197             # Insert the object index entry.
0198             indexnode = addnodes.index()
0199             indexnode['entries'] = [make_index_entry(title, targetid)]
0200             self.document.insert(0, indexnode)
0201             # Add to ecm domain object inventory
0202             _ecm_object_inventory(env, self.document, 1, objtype, targetid)
0203 
0204 class ECMObject(ObjectDescription):
0205 
0206     def handle_signature(self, sig, signode):
0207         # called from sphinx.directives.ObjectDescription.run()
0208         signode += addnodes.desc_name(sig, sig)
0209         return sig
0210 
0211     def add_target_and_index(self, name, sig, signode):
0212         targetid = '%s:%s' % (self.objtype, name)
0213         if targetid not in self.state.document.ids:
0214             signode['names'].append(targetid)
0215             signode['ids'].append(targetid)
0216             signode['first'] = (not self.names)
0217             self.state.document.note_explicit_target(signode)
0218             _ecm_object_inventory(self.env, self.state.document,
0219                                     self.lineno, self.objtype, targetid)
0220 
0221         make_index_entry = _ecm_index_objs.get(self.objtype)
0222         if make_index_entry:
0223             self.indexnode['entries'].append(make_index_entry(name, targetid))
0224 
0225 class ECMXRefRole(XRefRole):
0226 
0227     # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'.
0228     _re = re.compile(r'^(.+?)(\s*)(?<!\x00)<(.*?)>$', re.DOTALL)
0229     _re_sub = re.compile(r'^([^()\s]+)\s*\(([^()]*)\)$', re.DOTALL)
0230 
0231     def __call__(self, typ, rawtext, text, *args, **keys):
0232         # CMake cross-reference targets may contain '<' so escape
0233         # any explicit `<target>` with '<' not preceded by whitespace.
0234         while True:
0235             m = ECMXRefRole._re.match(text)
0236             if m and len(m.group(2)) == 0:
0237                 text = '%s\x00<%s>' % (m.group(1), m.group(3))
0238             else:
0239                 break
0240         return XRefRole.__call__(self, typ, rawtext, text, *args, **keys)
0241 
0242 class ECMDomain(Domain):
0243     """ECM domain."""
0244     name = 'ecm'
0245     label = 'ECM'
0246     object_types = {
0247         'module':      ObjType('module',      'module'),
0248         'kde-module':  ObjType('kde-module',  'kde-module'),
0249         'find-module': ObjType('find-module', 'find-module'),
0250         'manual':      ObjType('manual',      'manual'),
0251         'toolchain':   ObjType('toolchain',   'toolchain'),
0252     }
0253     directives = {}
0254     roles = {
0255         'module':      XRefRole(),
0256         'kde-module':  XRefRole(),
0257         'find-module': XRefRole(),
0258         'manual':      XRefRole(),
0259         'toolchain':   XRefRole(),
0260     }
0261     initial_data = {
0262         'objects': {},  # fullname -> docname, objtype
0263     }
0264 
0265     def clear_doc(self, docname):
0266         to_clear = []
0267         for fullname, (fn, _) in self.data['objects'].items():
0268             if fn == docname:
0269                 to_clear.append(fullname)
0270         for fullname in to_clear:
0271             del self.data['objects'][fullname]
0272 
0273     def resolve_xref(self, env, fromdocname, builder,
0274                      typ, target, node, contnode):
0275         targetid = '%s:%s' % (typ, target)
0276         obj = self.data['objects'].get(targetid)
0277         if obj is None:
0278             # TODO: warn somehow?
0279             return None
0280         return make_refnode(builder, fromdocname, obj[0], targetid,
0281                             contnode, target)
0282 
0283     def get_objects(self):
0284         for refname, (docname, type) in self.data['objects'].items():
0285             yield (refname, refname, type, docname, refname, 1)
0286 
0287 def setup(app):
0288     app.add_directive('ecm-module', ECMModule)
0289     app.add_transform(ECMTransform)
0290     app.add_domain(ECMDomain)