1 # Distributed under the OSI-approved BSD 3-Clause License. See accompanying
2 # file Copyright.txt or https://cmake.org/licensing for details.
7 # Override much of pygments' CMakeLexer.
8 # We need to parse CMake syntax definitions, not CMake code.
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, "::")
15 from pygments.lexers import CMakeLexer
16 from pygments.token import Name, Operator, Punctuation, String, Text, Comment, Generic, Whitespace, Number
17 from pygments.lexer import bygroups
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.
27 CMakeLexer.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
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'),
55 (r'[ \t]+', Whitespace),
57 # (r'[^<>\])\}\|$"# \t\n]+', Name.Exception), # fallback, for debugging only
60 from docutils.parsers.rst import Directive, directives
61 from docutils.transforms import Transform
62 from docutils import io, nodes
64 from sphinx.directives import ObjectDescription
65 from sphinx.domains import Domain, ObjType
66 from sphinx.roles import XRefRole
67 from sphinx.util.nodes import make_refnode
68 from sphinx import addnodes
70 sphinx_before_1_4 = False
71 sphinx_before_1_7_2 = False
73 from sphinx import version_info
74 if version_info < (1, 4):
75 sphinx_before_1_4 = True
76 if version_info < (1, 7, 2):
77 sphinx_before_1_7_2 = True
79 # The `sphinx.version_info` tuple was added in Sphinx v1.2:
80 sphinx_before_1_4 = True
81 sphinx_before_1_7_2 = True
83 if sphinx_before_1_7_2:
84 # Monkey patch for sphinx generating invalid content for qcollectiongenerator
85 # https://github.com/sphinx-doc/sphinx/issues/1435
86 from sphinx.util.pycompat import htmlescape
87 from sphinx.builders.qthelp import QtHelpBuilder
88 old_build_keywords = QtHelpBuilder.build_keywords
89 def new_build_keywords(self, title, refs, subitems):
90 old_items = old_build_keywords(self, title, refs, subitems)
92 for item in old_items:
93 before, rest = item.split("ref=\"", 1)
94 ref, after = rest.split("\"")
95 if ("<" in ref and ">" in ref):
96 new_items.append(before + "ref=\"" + htmlescape(ref) + "\"" + after)
98 new_items.append(item)
100 QtHelpBuilder.build_keywords = new_build_keywords
102 class CMakeModule(Directive):
103 required_arguments = 1
104 optional_arguments = 0
105 final_argument_whitespace = True
106 option_spec = {'encoding': directives.encoding}
108 def __init__(self, *args, **keys):
109 self.re_start = re.compile(r'^#\[(?P<eq>=*)\[\.rst:$')
110 Directive.__init__(self, *args, **keys)
113 settings = self.state.document.settings
114 if not settings.file_insertion_enabled:
115 raise self.warning('"%s" directive disabled.' % self.name)
117 env = self.state.document.settings.env
118 rel_path, path = env.relfn2path(self.arguments[0])
119 path = os.path.normpath(path)
120 encoding = self.options.get('encoding', settings.input_encoding)
121 e_handler = settings.input_encoding_error_handler
123 settings.record_dependencies.add(path)
124 f = io.FileInput(source_path=path, encoding=encoding,
125 error_handler=e_handler)
126 except UnicodeEncodeError as error:
127 msg = ('Problems with "%s" directive path:\n'
128 'Cannot encode input file path "%s" '
129 '(wrong locale?).' % (self.name, path))
130 raise self.severe(msg)
131 except IOError as error:
132 msg = 'Problems with "%s" directive path:\n%s.' % (self.name, error)
133 raise self.severe(msg)
134 raw_lines = f.read().splitlines()
138 for line in raw_lines:
139 if rst is not None and rst != '#':
140 # Bracket mode: check for end bracket
149 # Line mode: check for .rst start (bracket or line)
150 m = self.re_start.match(line)
152 rst = ']%s]' % m.group('eq')
154 elif line == '#.rst:':
158 if line == '#' or line[:2] == '# ':
166 if rst is not None and rst != '#':
167 raise self.warning('"%s" found unclosed bracket "#[%s[.rst:" in %s' %
168 (self.name, rst[1:-1], path))
169 self.state_machine.insert_input(lines, path)
172 class _cmake_index_entry:
173 def __init__(self, desc):
176 def __call__(self, title, targetid, main = 'main'):
177 # See https://github.com/sphinx-doc/sphinx/issues/2673
178 if sphinx_before_1_4:
179 return ('pair', u'%s ; %s' % (self.desc, title), targetid, main)
181 return ('pair', u'%s ; %s' % (self.desc, title), targetid, main, None)
183 _cmake_index_objs = {
184 'command': _cmake_index_entry('command'),
185 'cpack_gen': _cmake_index_entry('cpack generator'),
186 'envvar': _cmake_index_entry('envvar'),
187 'generator': _cmake_index_entry('generator'),
188 'genex': _cmake_index_entry('genex'),
189 'guide': _cmake_index_entry('guide'),
190 'manual': _cmake_index_entry('manual'),
191 'module': _cmake_index_entry('module'),
192 'policy': _cmake_index_entry('policy'),
193 'prop_cache': _cmake_index_entry('cache property'),
194 'prop_dir': _cmake_index_entry('directory property'),
195 'prop_gbl': _cmake_index_entry('global property'),
196 'prop_inst': _cmake_index_entry('installed file property'),
197 'prop_sf': _cmake_index_entry('source file property'),
198 'prop_test': _cmake_index_entry('test property'),
199 'prop_tgt': _cmake_index_entry('target property'),
200 'variable': _cmake_index_entry('variable'),
203 def _cmake_object_inventory(env, document, line, objtype, targetid):
204 inv = env.domaindata['cmake']['objects']
206 document.reporter.warning(
207 'CMake object "%s" also described in "%s".' %
208 (targetid, env.doc2path(inv[targetid][0])), line=line)
209 inv[targetid] = (env.docname, objtype)
211 class CMakeTransform(Transform):
213 # Run this transform early since we insert nodes we want
214 # treated as if they were written in the documents.
215 default_priority = 210
217 def __init__(self, document, startnode):
218 Transform.__init__(self, document, startnode)
221 def parse_title(self, docname):
222 """Parse a document title as the first line starting in [A-Za-z0-9<$]
223 or fall back to the document basename if no such line exists.
224 The cmake --help-*-list commands also depend on this convention.
225 Return the title or False if the document file does not exist.
227 env = self.document.settings.env
228 title = self.titles.get(docname)
230 fname = os.path.join(env.srcdir, docname+'.rst')
237 if len(line) > 0 and (line[0].isalnum() or line[0] == '<' or line[0] == '$'):
238 title = line.rstrip()
242 title = os.path.basename(docname)
243 self.titles[docname] = title
247 env = self.document.settings.env
249 # Treat some documents as cmake domain objects.
250 objtype, sep, tail = env.docname.partition('/')
251 make_index_entry = _cmake_index_objs.get(objtype)
253 title = self.parse_title(env.docname)
254 # Insert the object link target.
255 if objtype == 'command':
256 targetname = title.lower()
257 elif objtype == 'guide' and not tail.endswith('/index'):
260 if objtype == 'genex':
261 m = CMakeXRefRole._re_genex.match(title)
265 targetid = '%s:%s' % (objtype, targetname)
266 targetnode = nodes.target('', '', ids=[targetid])
267 self.document.note_explicit_target(targetnode)
268 self.document.insert(0, targetnode)
269 # Insert the object index entry.
270 indexnode = addnodes.index()
271 indexnode['entries'] = [make_index_entry(title, targetid)]
272 self.document.insert(0, indexnode)
273 # Add to cmake domain object inventory
274 _cmake_object_inventory(env, self.document, 1, objtype, targetid)
276 class CMakeObject(ObjectDescription):
278 def handle_signature(self, sig, signode):
279 # called from sphinx.directives.ObjectDescription.run()
280 signode += addnodes.desc_name(sig, sig)
281 if self.objtype == 'genex':
282 m = CMakeXRefRole._re_genex.match(sig)
287 def add_target_and_index(self, name, sig, signode):
288 if self.objtype == 'command':
289 targetname = name.lower()
292 targetid = '%s:%s' % (self.objtype, targetname)
293 if targetid not in self.state.document.ids:
294 signode['names'].append(targetid)
295 signode['ids'].append(targetid)
296 signode['first'] = (not self.names)
297 self.state.document.note_explicit_target(signode)
298 _cmake_object_inventory(self.env, self.state.document,
299 self.lineno, self.objtype, targetid)
301 make_index_entry = _cmake_index_objs.get(self.objtype)
303 self.indexnode['entries'].append(make_index_entry(name, targetid))
305 class CMakeXRefRole(XRefRole):
307 # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'.
308 _re = re.compile(r'^(.+?)(\s*)(?<!\x00)<(.*?)>$', re.DOTALL)
309 _re_sub = re.compile(r'^([^()\s]+)\s*\(([^()]*)\)$', re.DOTALL)
310 _re_genex = re.compile(r'^\$<([^<>:]+)(:[^<>]+)?>$', re.DOTALL)
311 _re_guide = re.compile(r'^([^<>/]+)/([^<>]*)$', re.DOTALL)
313 def __call__(self, typ, rawtext, text, *args, **keys):
314 # Translate CMake command cross-references of the form:
315 # `command_name(SUB_COMMAND)`
316 # to have an explicit target:
317 # `command_name(SUB_COMMAND) <command_name>`
318 if typ == 'cmake:command':
319 m = CMakeXRefRole._re_sub.match(text)
321 text = '%s <%s>' % (text, m.group(1))
322 elif typ == 'cmake:genex':
323 m = CMakeXRefRole._re_genex.match(text)
325 text = '%s <%s>' % (text, m.group(1))
326 elif typ == 'cmake:guide':
327 m = CMakeXRefRole._re_guide.match(text)
329 text = '%s <%s>' % (m.group(2), text)
330 # CMake cross-reference targets frequently contain '<' so escape
331 # any explicit `<target>` with '<' not preceded by whitespace.
333 m = CMakeXRefRole._re.match(text)
334 if m and len(m.group(2)) == 0:
335 text = '%s\x00<%s>' % (m.group(1), m.group(3))
338 return XRefRole.__call__(self, typ, rawtext, text, *args, **keys)
340 # We cannot insert index nodes using the result_nodes method
341 # because CMakeXRefRole is processed before substitution_reference
342 # nodes are evaluated so target nodes (with 'ids' fields) would be
343 # duplicated in each evaluated substitution replacement. The
344 # docutils substitution transform does not allow this. Instead we
345 # use our own CMakeXRefTransform below to add index entries after
346 # substitutions are completed.
348 # def result_nodes(self, document, env, node, is_ref):
351 class CMakeXRefTransform(Transform):
353 # Run this transform early since we insert nodes we want
354 # treated as if they were written in the documents, but
355 # after the sphinx (210) and docutils (220) substitutions.
356 default_priority = 221
359 env = self.document.settings.env
361 # Find CMake cross-reference nodes and add index and target
363 for ref in self.document.traverse(addnodes.pending_xref):
364 if not ref['refdomain'] == 'cmake':
367 objtype = ref['reftype']
368 make_index_entry = _cmake_index_objs.get(objtype)
369 if not make_index_entry:
372 objname = ref['reftarget']
373 if objtype == 'guide' and CMakeXRefRole._re_guide.match(objname):
374 # Do not index cross-references to guide sections.
377 targetnum = env.new_serialno('index-%s:%s' % (objtype, objname))
379 targetid = 'index-%s-%s:%s' % (targetnum, objtype, objname)
380 targetnode = nodes.target('', '', ids=[targetid])
381 self.document.note_explicit_target(targetnode)
383 indexnode = addnodes.index()
384 indexnode['entries'] = [make_index_entry(objname, targetid, '')]
385 ref.replace_self([indexnode, targetnode, ref])
387 class CMakeDomain(Domain):
392 'command': ObjType('command', 'command'),
393 'cpack_gen': ObjType('cpack_gen', 'cpack_gen'),
394 'envvar': ObjType('envvar', 'envvar'),
395 'generator': ObjType('generator', 'generator'),
396 'genex': ObjType('genex', 'genex'),
397 'guide': ObjType('guide', 'guide'),
398 'variable': ObjType('variable', 'variable'),
399 'module': ObjType('module', 'module'),
400 'policy': ObjType('policy', 'policy'),
401 'prop_cache': ObjType('prop_cache', 'prop_cache'),
402 'prop_dir': ObjType('prop_dir', 'prop_dir'),
403 'prop_gbl': ObjType('prop_gbl', 'prop_gbl'),
404 'prop_inst': ObjType('prop_inst', 'prop_inst'),
405 'prop_sf': ObjType('prop_sf', 'prop_sf'),
406 'prop_test': ObjType('prop_test', 'prop_test'),
407 'prop_tgt': ObjType('prop_tgt', 'prop_tgt'),
408 'manual': ObjType('manual', 'manual'),
411 'command': CMakeObject,
412 'envvar': CMakeObject,
413 'genex': CMakeObject,
414 'variable': CMakeObject,
415 # Other object types cannot be created except by the CMakeTransform
416 # 'generator': CMakeObject,
417 # 'module': CMakeObject,
418 # 'policy': CMakeObject,
419 # 'prop_cache': CMakeObject,
420 # 'prop_dir': CMakeObject,
421 # 'prop_gbl': CMakeObject,
422 # 'prop_inst': CMakeObject,
423 # 'prop_sf': CMakeObject,
424 # 'prop_test': CMakeObject,
425 # 'prop_tgt': CMakeObject,
426 # 'manual': CMakeObject,
429 'command': CMakeXRefRole(fix_parens = True, lowercase = True),
430 'cpack_gen': CMakeXRefRole(),
431 'envvar': CMakeXRefRole(),
432 'generator': CMakeXRefRole(),
433 'genex': CMakeXRefRole(),
434 'guide': CMakeXRefRole(),
435 'variable': CMakeXRefRole(),
436 'module': CMakeXRefRole(),
437 'policy': CMakeXRefRole(),
438 'prop_cache': CMakeXRefRole(),
439 'prop_dir': CMakeXRefRole(),
440 'prop_gbl': CMakeXRefRole(),
441 'prop_inst': CMakeXRefRole(),
442 'prop_sf': CMakeXRefRole(),
443 'prop_test': CMakeXRefRole(),
444 'prop_tgt': CMakeXRefRole(),
445 'manual': CMakeXRefRole(),
448 'objects': {}, # fullname -> docname, objtype
451 def clear_doc(self, docname):
453 for fullname, (fn, _) in self.data['objects'].items():
455 to_clear.add(fullname)
456 for fullname in to_clear:
457 del self.data['objects'][fullname]
459 def resolve_xref(self, env, fromdocname, builder,
460 typ, target, node, contnode):
461 targetid = '%s:%s' % (typ, target)
462 obj = self.data['objects'].get(targetid)
464 # TODO: warn somehow?
466 return make_refnode(builder, fromdocname, obj[0], targetid,
469 def get_objects(self):
470 for refname, (docname, type) in self.data['objects'].items():
471 yield (refname, refname, type, docname, refname, 1)
474 app.add_directive('cmake-module', CMakeModule)
475 app.add_transform(CMakeTransform)
476 app.add_transform(CMakeXRefTransform)
477 app.add_domain(CMakeDomain)