File indexing completed on 2025-04-20 09:38:04
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)