1# Distributed under the OSI-approved BSD 3-Clause License. See accompanying 2# file Copyright.txt or https://cmake.org/licensing for details. 3 4import os 5import re 6 7# Override much of pygments' CMakeLexer. 8# We need to parse CMake syntax definitions, not CMake code. 9 10# For hard test cases that use much of the syntax below, see 11# - module/FindPkgConfig.html (with "glib-2.0>=2.10 gtk+-2.0" and similar) 12# - module/ExternalProject.html (with http:// https:// git@; also has command options -E --build) 13# - manual/cmake-buildsystem.7.html (with nested $<..>; relative and absolute paths, "::") 14 15from pygments.lexers import CMakeLexer 16from pygments.token import Name, Operator, Punctuation, String, Text, Comment, Generic, Whitespace, Number 17from pygments.lexer import bygroups 18 19# Notes on regular expressions below: 20# - [\.\+-] are needed for string constants like gtk+-2.0 21# - Unix paths are recognized by '/'; support for Windows paths may be added if needed 22# - (\\.) allows for \-escapes (used in manual/cmake-language.7) 23# - $<..$<..$>..> nested occurrence in cmake-buildsystem 24# - Nested variable evaluations are only supported in a limited capacity. Only 25# one level of nesting is supported and at most one nested variable can be present. 26 27CMakeLexer.tokens["root"] = [ 28 (r'\b(\w+)([ \t]*)(\()', bygroups(Name.Function, Text, Name.Function), '#push'), # fctn( 29 (r'\(', Name.Function, '#push'), 30 (r'\)', Name.Function, '#pop'), 31 (r'\[', Punctuation, '#push'), 32 (r'\]', Punctuation, '#pop'), 33 (r'[|;,.=*\-]', Punctuation), 34 (r'\\\\', Punctuation), # used in commands/source_group 35 (r'[:]', Operator), 36 (r'[<>]=', Punctuation), # used in FindPkgConfig.cmake 37 (r'\$<', Operator, '#push'), # $<...> 38 (r'<[^<|]+?>(\w*\.\.\.)?', Name.Variable), # <expr> 39 (r'(\$\w*\{)([^\}\$]*)?(?:(\$\w*\{)([^\}]+?)(\}))?([^\}]*?)(\})', # ${..} $ENV{..}, possibly nested 40 bygroups(Operator, Name.Tag, Operator, Name.Tag, Operator, Name.Tag, Operator)), 41 (r'([A-Z]+\{)(.+?)(\})', bygroups(Operator, Name.Tag, Operator)), # DATA{ ...} 42 (r'[a-z]+(@|(://))((\\.)|[\w.+-:/\\])+', Name.Attribute), # URL, git@, ... 43 (r'/\w[\w\.\+-/\\]*', Name.Attribute), # absolute path 44 (r'/', Name.Attribute), 45 (r'\w[\w\.\+-]*/[\w.+-/\\]*', Name.Attribute), # relative path 46 (r'[A-Z]((\\.)|[\w.+-])*[a-z]((\\.)|[\w.+-])*', Name.Builtin), # initial A-Z, contains a-z 47 (r'@?[A-Z][A-Z0-9_]*', Name.Constant), 48 (r'[a-z_]((\\;)|(\\ )|[\w.+-])*', Name.Builtin), 49 (r'[0-9][0-9\.]*', Number), 50 (r'(?s)"(\\"|[^"])*"', String), # "string" 51 (r'\.\.\.', Name.Variable), 52 (r'<', Operator, '#push'), # <..|..> is different from <expr> 53 (r'>', Operator, '#pop'), 54 (r'\n', Whitespace), 55 (r'[ \t]+', Whitespace), 56 (r'#.*\n', Comment), 57 # (r'[^<>\])\}\|$"# \t\n]+', Name.Exception), # fallback, for debugging only 58] 59 60from docutils.parsers.rst import Directive, directives 61from docutils.transforms import Transform 62try: 63 from docutils.utils.error_reporting import SafeString, ErrorString 64except ImportError: 65 # error_reporting was not in utils before version 0.11: 66 from docutils.error_reporting import SafeString, ErrorString 67 68from docutils import io, nodes 69 70from sphinx.directives import ObjectDescription 71from sphinx.domains import Domain, ObjType 72from sphinx.roles import XRefRole 73from sphinx.util.nodes import make_refnode 74from sphinx import addnodes 75 76sphinx_before_1_4 = False 77sphinx_before_1_7_2 = False 78try: 79 from sphinx import version_info 80 if version_info < (1, 4): 81 sphinx_before_1_4 = True 82 if version_info < (1, 7, 2): 83 sphinx_before_1_7_2 = True 84except ImportError: 85 # The `sphinx.version_info` tuple was added in Sphinx v1.2: 86 sphinx_before_1_4 = True 87 sphinx_before_1_7_2 = True 88 89if sphinx_before_1_7_2: 90 # Monkey patch for sphinx generating invalid content for qcollectiongenerator 91 # https://github.com/sphinx-doc/sphinx/issues/1435 92 from sphinx.util.pycompat import htmlescape 93 from sphinx.builders.qthelp import QtHelpBuilder 94 old_build_keywords = QtHelpBuilder.build_keywords 95 def new_build_keywords(self, title, refs, subitems): 96 old_items = old_build_keywords(self, title, refs, subitems) 97 new_items = [] 98 for item in old_items: 99 before, rest = item.split("ref=\"", 1) 100 ref, after = rest.split("\"") 101 if ("<" in ref and ">" in ref): 102 new_items.append(before + "ref=\"" + htmlescape(ref) + "\"" + after) 103 else: 104 new_items.append(item) 105 return new_items 106 QtHelpBuilder.build_keywords = new_build_keywords 107 108class CMakeModule(Directive): 109 required_arguments = 1 110 optional_arguments = 0 111 final_argument_whitespace = True 112 option_spec = {'encoding': directives.encoding} 113 114 def __init__(self, *args, **keys): 115 self.re_start = re.compile(r'^#\[(?P<eq>=*)\[\.rst:$') 116 Directive.__init__(self, *args, **keys) 117 118 def run(self): 119 settings = self.state.document.settings 120 if not settings.file_insertion_enabled: 121 raise self.warning('"%s" directive disabled.' % self.name) 122 123 env = self.state.document.settings.env 124 rel_path, path = env.relfn2path(self.arguments[0]) 125 path = os.path.normpath(path) 126 encoding = self.options.get('encoding', settings.input_encoding) 127 e_handler = settings.input_encoding_error_handler 128 try: 129 settings.record_dependencies.add(path) 130 f = io.FileInput(source_path=path, encoding=encoding, 131 error_handler=e_handler) 132 except UnicodeEncodeError as error: 133 raise self.severe('Problems with "%s" directive path:\n' 134 'Cannot encode input file path "%s" ' 135 '(wrong locale?).' % 136 (self.name, SafeString(path))) 137 except IOError as error: 138 raise self.severe('Problems with "%s" directive path:\n%s.' % 139 (self.name, ErrorString(error))) 140 raw_lines = f.read().splitlines() 141 f.close() 142 rst = None 143 lines = [] 144 for line in raw_lines: 145 if rst is not None and rst != '#': 146 # Bracket mode: check for end bracket 147 pos = line.find(rst) 148 if pos >= 0: 149 if line[0] == '#': 150 line = '' 151 else: 152 line = line[0:pos] 153 rst = None 154 else: 155 # Line mode: check for .rst start (bracket or line) 156 m = self.re_start.match(line) 157 if m: 158 rst = ']%s]' % m.group('eq') 159 line = '' 160 elif line == '#.rst:': 161 rst = '#' 162 line = '' 163 elif rst == '#': 164 if line == '#' or line[:2] == '# ': 165 line = line[2:] 166 else: 167 rst = None 168 line = '' 169 elif rst is None: 170 line = '' 171 lines.append(line) 172 if rst is not None and rst != '#': 173 raise self.warning('"%s" found unclosed bracket "#[%s[.rst:" in %s' % 174 (self.name, rst[1:-1], path)) 175 self.state_machine.insert_input(lines, path) 176 return [] 177 178class _cmake_index_entry: 179 def __init__(self, desc): 180 self.desc = desc 181 182 def __call__(self, title, targetid, main = 'main'): 183 # See https://github.com/sphinx-doc/sphinx/issues/2673 184 if sphinx_before_1_4: 185 return ('pair', u'%s ; %s' % (self.desc, title), targetid, main) 186 else: 187 return ('pair', u'%s ; %s' % (self.desc, title), targetid, main, None) 188 189_cmake_index_objs = { 190 'command': _cmake_index_entry('command'), 191 'cpack_gen': _cmake_index_entry('cpack generator'), 192 'envvar': _cmake_index_entry('envvar'), 193 'generator': _cmake_index_entry('generator'), 194 'genex': _cmake_index_entry('genex'), 195 'guide': _cmake_index_entry('guide'), 196 'manual': _cmake_index_entry('manual'), 197 'module': _cmake_index_entry('module'), 198 'policy': _cmake_index_entry('policy'), 199 'prop_cache': _cmake_index_entry('cache property'), 200 'prop_dir': _cmake_index_entry('directory property'), 201 'prop_gbl': _cmake_index_entry('global property'), 202 'prop_inst': _cmake_index_entry('installed file property'), 203 'prop_sf': _cmake_index_entry('source file property'), 204 'prop_test': _cmake_index_entry('test property'), 205 'prop_tgt': _cmake_index_entry('target property'), 206 'variable': _cmake_index_entry('variable'), 207 } 208 209def _cmake_object_inventory(env, document, line, objtype, targetid): 210 inv = env.domaindata['cmake']['objects'] 211 if targetid in inv: 212 document.reporter.warning( 213 'CMake object "%s" also described in "%s".' % 214 (targetid, env.doc2path(inv[targetid][0])), line=line) 215 inv[targetid] = (env.docname, objtype) 216 217class CMakeTransform(Transform): 218 219 # Run this transform early since we insert nodes we want 220 # treated as if they were written in the documents. 221 default_priority = 210 222 223 def __init__(self, document, startnode): 224 Transform.__init__(self, document, startnode) 225 self.titles = {} 226 227 def parse_title(self, docname): 228 """Parse a document title as the first line starting in [A-Za-z0-9<$] 229 or fall back to the document basename if no such line exists. 230 The cmake --help-*-list commands also depend on this convention. 231 Return the title or False if the document file does not exist. 232 """ 233 env = self.document.settings.env 234 title = self.titles.get(docname) 235 if title is None: 236 fname = os.path.join(env.srcdir, docname+'.rst') 237 try: 238 f = open(fname, 'r') 239 except IOError: 240 title = False 241 else: 242 for line in f: 243 if len(line) > 0 and (line[0].isalnum() or line[0] == '<' or line[0] == '$'): 244 title = line.rstrip() 245 break 246 f.close() 247 if title is None: 248 title = os.path.basename(docname) 249 self.titles[docname] = title 250 return title 251 252 def apply(self): 253 env = self.document.settings.env 254 255 # Treat some documents as cmake domain objects. 256 objtype, sep, tail = env.docname.partition('/') 257 make_index_entry = _cmake_index_objs.get(objtype) 258 if make_index_entry: 259 title = self.parse_title(env.docname) 260 # Insert the object link target. 261 if objtype == 'command': 262 targetname = title.lower() 263 elif objtype == 'guide' and not tail.endswith('/index'): 264 targetname = tail 265 else: 266 if objtype == 'genex': 267 m = CMakeXRefRole._re_genex.match(title) 268 if m: 269 title = m.group(1) 270 targetname = title 271 targetid = '%s:%s' % (objtype, targetname) 272 targetnode = nodes.target('', '', ids=[targetid]) 273 self.document.note_explicit_target(targetnode) 274 self.document.insert(0, targetnode) 275 # Insert the object index entry. 276 indexnode = addnodes.index() 277 indexnode['entries'] = [make_index_entry(title, targetid)] 278 self.document.insert(0, indexnode) 279 # Add to cmake domain object inventory 280 _cmake_object_inventory(env, self.document, 1, objtype, targetid) 281 282class CMakeObject(ObjectDescription): 283 284 def handle_signature(self, sig, signode): 285 # called from sphinx.directives.ObjectDescription.run() 286 signode += addnodes.desc_name(sig, sig) 287 if self.objtype == 'genex': 288 m = CMakeXRefRole._re_genex.match(sig) 289 if m: 290 sig = m.group(1) 291 return sig 292 293 def add_target_and_index(self, name, sig, signode): 294 if self.objtype == 'command': 295 targetname = name.lower() 296 else: 297 targetname = name 298 targetid = '%s:%s' % (self.objtype, targetname) 299 if targetid not in self.state.document.ids: 300 signode['names'].append(targetid) 301 signode['ids'].append(targetid) 302 signode['first'] = (not self.names) 303 self.state.document.note_explicit_target(signode) 304 _cmake_object_inventory(self.env, self.state.document, 305 self.lineno, self.objtype, targetid) 306 307 make_index_entry = _cmake_index_objs.get(self.objtype) 308 if make_index_entry: 309 self.indexnode['entries'].append(make_index_entry(name, targetid)) 310 311class CMakeXRefRole(XRefRole): 312 313 # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'. 314 _re = re.compile(r'^(.+?)(\s*)(?<!\x00)<(.*?)>$', re.DOTALL) 315 _re_sub = re.compile(r'^([^()\s]+)\s*\(([^()]*)\)$', re.DOTALL) 316 _re_genex = re.compile(r'^\$<([^<>:]+)(:[^<>]+)?>$', re.DOTALL) 317 _re_guide = re.compile(r'^([^<>/]+)/([^<>]*)$', re.DOTALL) 318 319 def __call__(self, typ, rawtext, text, *args, **keys): 320 # Translate CMake command cross-references of the form: 321 # `command_name(SUB_COMMAND)` 322 # to have an explicit target: 323 # `command_name(SUB_COMMAND) <command_name>` 324 if typ == 'cmake:command': 325 m = CMakeXRefRole._re_sub.match(text) 326 if m: 327 text = '%s <%s>' % (text, m.group(1)) 328 elif typ == 'cmake:genex': 329 m = CMakeXRefRole._re_genex.match(text) 330 if m: 331 text = '%s <%s>' % (text, m.group(1)) 332 elif typ == 'cmake:guide': 333 m = CMakeXRefRole._re_guide.match(text) 334 if m: 335 text = '%s <%s>' % (m.group(2), text) 336 # CMake cross-reference targets frequently contain '<' so escape 337 # any explicit `<target>` with '<' not preceded by whitespace. 338 while True: 339 m = CMakeXRefRole._re.match(text) 340 if m and len(m.group(2)) == 0: 341 text = '%s\x00<%s>' % (m.group(1), m.group(3)) 342 else: 343 break 344 return XRefRole.__call__(self, typ, rawtext, text, *args, **keys) 345 346 # We cannot insert index nodes using the result_nodes method 347 # because CMakeXRefRole is processed before substitution_reference 348 # nodes are evaluated so target nodes (with 'ids' fields) would be 349 # duplicated in each evaluated substitution replacement. The 350 # docutils substitution transform does not allow this. Instead we 351 # use our own CMakeXRefTransform below to add index entries after 352 # substitutions are completed. 353 # 354 # def result_nodes(self, document, env, node, is_ref): 355 # pass 356 357class CMakeXRefTransform(Transform): 358 359 # Run this transform early since we insert nodes we want 360 # treated as if they were written in the documents, but 361 # after the sphinx (210) and docutils (220) substitutions. 362 default_priority = 221 363 364 def apply(self): 365 env = self.document.settings.env 366 367 # Find CMake cross-reference nodes and add index and target 368 # nodes for them. 369 for ref in self.document.traverse(addnodes.pending_xref): 370 if not ref['refdomain'] == 'cmake': 371 continue 372 373 objtype = ref['reftype'] 374 make_index_entry = _cmake_index_objs.get(objtype) 375 if not make_index_entry: 376 continue 377 378 objname = ref['reftarget'] 379 if objtype == 'guide' and CMakeXRefRole._re_guide.match(objname): 380 # Do not index cross-references to guide sections. 381 continue 382 383 targetnum = env.new_serialno('index-%s:%s' % (objtype, objname)) 384 385 targetid = 'index-%s-%s:%s' % (targetnum, objtype, objname) 386 targetnode = nodes.target('', '', ids=[targetid]) 387 self.document.note_explicit_target(targetnode) 388 389 indexnode = addnodes.index() 390 indexnode['entries'] = [make_index_entry(objname, targetid, '')] 391 ref.replace_self([indexnode, targetnode, ref]) 392 393class CMakeDomain(Domain): 394 """CMake domain.""" 395 name = 'cmake' 396 label = 'CMake' 397 object_types = { 398 'command': ObjType('command', 'command'), 399 'cpack_gen': ObjType('cpack_gen', 'cpack_gen'), 400 'envvar': ObjType('envvar', 'envvar'), 401 'generator': ObjType('generator', 'generator'), 402 'genex': ObjType('genex', 'genex'), 403 'guide': ObjType('guide', 'guide'), 404 'variable': ObjType('variable', 'variable'), 405 'module': ObjType('module', 'module'), 406 'policy': ObjType('policy', 'policy'), 407 'prop_cache': ObjType('prop_cache', 'prop_cache'), 408 'prop_dir': ObjType('prop_dir', 'prop_dir'), 409 'prop_gbl': ObjType('prop_gbl', 'prop_gbl'), 410 'prop_inst': ObjType('prop_inst', 'prop_inst'), 411 'prop_sf': ObjType('prop_sf', 'prop_sf'), 412 'prop_test': ObjType('prop_test', 'prop_test'), 413 'prop_tgt': ObjType('prop_tgt', 'prop_tgt'), 414 'manual': ObjType('manual', 'manual'), 415 } 416 directives = { 417 'command': CMakeObject, 418 'envvar': CMakeObject, 419 'genex': CMakeObject, 420 'variable': CMakeObject, 421 # Other object types cannot be created except by the CMakeTransform 422 # 'generator': CMakeObject, 423 # 'module': CMakeObject, 424 # 'policy': CMakeObject, 425 # 'prop_cache': CMakeObject, 426 # 'prop_dir': CMakeObject, 427 # 'prop_gbl': CMakeObject, 428 # 'prop_inst': CMakeObject, 429 # 'prop_sf': CMakeObject, 430 # 'prop_test': CMakeObject, 431 # 'prop_tgt': CMakeObject, 432 # 'manual': CMakeObject, 433 } 434 roles = { 435 'command': CMakeXRefRole(fix_parens = True, lowercase = True), 436 'cpack_gen': CMakeXRefRole(), 437 'envvar': CMakeXRefRole(), 438 'generator': CMakeXRefRole(), 439 'genex': CMakeXRefRole(), 440 'guide': CMakeXRefRole(), 441 'variable': CMakeXRefRole(), 442 'module': CMakeXRefRole(), 443 'policy': CMakeXRefRole(), 444 'prop_cache': CMakeXRefRole(), 445 'prop_dir': CMakeXRefRole(), 446 'prop_gbl': CMakeXRefRole(), 447 'prop_inst': CMakeXRefRole(), 448 'prop_sf': CMakeXRefRole(), 449 'prop_test': CMakeXRefRole(), 450 'prop_tgt': CMakeXRefRole(), 451 'manual': CMakeXRefRole(), 452 } 453 initial_data = { 454 'objects': {}, # fullname -> docname, objtype 455 } 456 457 def clear_doc(self, docname): 458 to_clear = set() 459 for fullname, (fn, _) in self.data['objects'].items(): 460 if fn == docname: 461 to_clear.add(fullname) 462 for fullname in to_clear: 463 del self.data['objects'][fullname] 464 465 def resolve_xref(self, env, fromdocname, builder, 466 typ, target, node, contnode): 467 targetid = '%s:%s' % (typ, target) 468 obj = self.data['objects'].get(targetid) 469 if obj is None: 470 # TODO: warn somehow? 471 return None 472 return make_refnode(builder, fromdocname, obj[0], targetid, 473 contnode, target) 474 475 def get_objects(self): 476 for refname, (docname, type) in self.data['objects'].items(): 477 yield (refname, refname, type, docname, refname, 1) 478 479def setup(app): 480 app.add_directive('cmake-module', CMakeModule) 481 app.add_transform(CMakeTransform) 482 app.add_transform(CMakeXRefTransform) 483 app.add_domain(CMakeDomain) 484