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
63 from docutils.utils.error_reporting import SafeString, ErrorString
65 # error_reporting was not in utils before version 0.11:
66 from docutils.error_reporting import SafeString, ErrorString
68 from docutils import io, nodes
70 from sphinx.directives import ObjectDescription
71 from sphinx.domains import Domain, ObjType
72 from sphinx.roles import XRefRole
73 from sphinx.util.nodes import make_refnode
74 from sphinx import addnodes
76 sphinx_before_1_4 = False
77 sphinx_before_1_7_2 = False
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
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
89 if 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)
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)
104 new_items.append(item)
106 QtHelpBuilder.build_keywords = new_build_keywords
108 class CMakeModule(Directive):
109 required_arguments = 1
110 optional_arguments = 0
111 final_argument_whitespace = True
112 option_spec = {'encoding': directives.encoding}
114 def __init__(self, *args, **keys):
115 self.re_start = re.compile(r'^#\[(?P<eq>=*)\[\.rst:$')
116 Directive.__init__(self, *args, **keys)
119 settings = self.state.document.settings
120 if not settings.file_insertion_enabled:
121 raise self.warning('"%s" directive disabled.' % self.name)
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
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" '
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()
144 for line in raw_lines:
145 if rst is not None and rst != '#':
146 # Bracket mode: check for end bracket
155 # Line mode: check for .rst start (bracket or line)
156 m = self.re_start.match(line)
158 rst = ']%s]' % m.group('eq')
160 elif line == '#.rst:':
164 if line == '#' or line[:2] == '# ':
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)
178 class _cmake_index_entry:
179 def __init__(self, desc):
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)
187 return ('pair', u'%s ; %s' % (self.desc, title), targetid, main, None)
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'),
209 def _cmake_object_inventory(env, document, line, objtype, targetid):
210 inv = env.domaindata['cmake']['objects']
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)
217 class CMakeTransform(Transform):
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
223 def __init__(self, document, startnode):
224 Transform.__init__(self, document, startnode)
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.
233 env = self.document.settings.env
234 title = self.titles.get(docname)
236 fname = os.path.join(env.srcdir, docname+'.rst')
243 if len(line) > 0 and (line[0].isalnum() or line[0] == '<' or line[0] == '$'):
244 title = line.rstrip()
248 title = os.path.basename(docname)
249 self.titles[docname] = title
253 env = self.document.settings.env
255 # Treat some documents as cmake domain objects.
256 objtype, sep, tail = env.docname.partition('/')
257 make_index_entry = _cmake_index_objs.get(objtype)
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'):
266 if objtype == 'genex':
267 m = CMakeXRefRole._re_genex.match(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)
282 class CMakeObject(ObjectDescription):
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)
293 def add_target_and_index(self, name, sig, signode):
294 if self.objtype == 'command':
295 targetname = name.lower()
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)
307 make_index_entry = _cmake_index_objs.get(self.objtype)
309 self.indexnode['entries'].append(make_index_entry(name, targetid))
311 class CMakeXRefRole(XRefRole):
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)
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)
327 text = '%s <%s>' % (text, m.group(1))
328 elif typ == 'cmake:genex':
329 m = CMakeXRefRole._re_genex.match(text)
331 text = '%s <%s>' % (text, m.group(1))
332 elif typ == 'cmake:guide':
333 m = CMakeXRefRole._re_guide.match(text)
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.
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))
344 return XRefRole.__call__(self, typ, rawtext, text, *args, **keys)
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.
354 # def result_nodes(self, document, env, node, is_ref):
357 class CMakeXRefTransform(Transform):
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
365 env = self.document.settings.env
367 # Find CMake cross-reference nodes and add index and target
369 for ref in self.document.traverse(addnodes.pending_xref):
370 if not ref['refdomain'] == 'cmake':
373 objtype = ref['reftype']
374 make_index_entry = _cmake_index_objs.get(objtype)
375 if not make_index_entry:
378 objname = ref['reftarget']
379 if objtype == 'guide' and CMakeXRefRole._re_guide.match(objname):
380 # Do not index cross-references to guide sections.
383 targetnum = env.new_serialno('index-%s:%s' % (objtype, objname))
385 targetid = 'index-%s-%s:%s' % (targetnum, objtype, objname)
386 targetnode = nodes.target('', '', ids=[targetid])
387 self.document.note_explicit_target(targetnode)
389 indexnode = addnodes.index()
390 indexnode['entries'] = [make_index_entry(objname, targetid, '')]
391 ref.replace_self([indexnode, targetnode, ref])
393 class CMakeDomain(Domain):
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'),
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,
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(),
454 'objects': {}, # fullname -> docname, objtype
457 def clear_doc(self, docname):
459 for fullname, (fn, _) in self.data['objects'].items():
461 to_clear.add(fullname)
462 for fullname in to_clear:
463 del self.data['objects'][fullname]
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)
470 # TODO: warn somehow?
472 return make_refnode(builder, fromdocname, obj[0], targetid,
475 def get_objects(self):
476 for refname, (docname, type) in self.data['objects'].items():
477 yield (refname, refname, type, docname, refname, 1)
480 app.add_directive('cmake-module', CMakeModule)
481 app.add_transform(CMakeTransform)
482 app.add_transform(CMakeXRefTransform)
483 app.add_domain(CMakeDomain)