Imported Upstream version 3.25.0
[platform/upstream/cmake.git] / Utilities / Sphinx / cmake.py
1 # Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2 # file Copyright.txt or https://cmake.org/licensing for details.
3
4 import os
5 import 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
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
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
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
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
60 from docutils.parsers.rst import Directive, directives
61 from docutils.transforms import Transform
62 from docutils import io, nodes
63
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
69
70 sphinx_before_1_4 = False
71 sphinx_before_1_7_2 = False
72 try:
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
78 except ImportError:
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
82
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)
91     new_items = []
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)
97       else:
98         new_items.append(item)
99     return new_items
100   QtHelpBuilder.build_keywords = new_build_keywords
101
102 class CMakeModule(Directive):
103     required_arguments = 1
104     optional_arguments = 0
105     final_argument_whitespace = True
106     option_spec = {'encoding': directives.encoding}
107
108     def __init__(self, *args, **keys):
109         self.re_start = re.compile(r'^#\[(?P<eq>=*)\[\.rst:$')
110         Directive.__init__(self, *args, **keys)
111
112     def run(self):
113         settings = self.state.document.settings
114         if not settings.file_insertion_enabled:
115             raise self.warning('"%s" directive disabled.' % self.name)
116
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
122         try:
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()
135         f.close()
136         rst = None
137         lines = []
138         for line in raw_lines:
139             if rst is not None and rst != '#':
140                 # Bracket mode: check for end bracket
141                 pos = line.find(rst)
142                 if pos >= 0:
143                     if line[0] == '#':
144                         line = ''
145                     else:
146                         line = line[0:pos]
147                     rst = None
148             else:
149                 # Line mode: check for .rst start (bracket or line)
150                 m = self.re_start.match(line)
151                 if m:
152                     rst = ']%s]' % m.group('eq')
153                     line = ''
154                 elif line == '#.rst:':
155                     rst = '#'
156                     line = ''
157                 elif rst == '#':
158                     if line == '#' or line[:2] == '# ':
159                         line = line[2:]
160                     else:
161                         rst = None
162                         line = ''
163                 elif rst is None:
164                     line = ''
165             lines.append(line)
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)
170         return []
171
172 class _cmake_index_entry:
173     def __init__(self, desc):
174         self.desc = desc
175
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)
180         else:
181             return ('pair', u'%s ; %s' % (self.desc, title), targetid, main, None)
182
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'),
201     }
202
203 def _cmake_object_inventory(env, document, line, objtype, targetid):
204     inv = env.domaindata['cmake']['objects']
205     if targetid in inv:
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)
210
211 class CMakeTransform(Transform):
212
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
216
217     def __init__(self, document, startnode):
218         Transform.__init__(self, document, startnode)
219         self.titles = {}
220
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.
226         """
227         env = self.document.settings.env
228         title = self.titles.get(docname)
229         if title is None:
230             fname = os.path.join(env.srcdir, docname+'.rst')
231             try:
232                 f = open(fname, 'r')
233             except IOError:
234                 title = False
235             else:
236                 for line in f:
237                     if len(line) > 0 and (line[0].isalnum() or line[0] == '<' or line[0] == '$'):
238                         title = line.rstrip()
239                         break
240                 f.close()
241                 if title is None:
242                     title = os.path.basename(docname)
243             self.titles[docname] = title
244         return title
245
246     def apply(self):
247         env = self.document.settings.env
248
249         # Treat some documents as cmake domain objects.
250         objtype, sep, tail = env.docname.partition('/')
251         make_index_entry = _cmake_index_objs.get(objtype)
252         if make_index_entry:
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'):
258                 targetname = tail
259             else:
260                 if objtype == 'genex':
261                     m = CMakeXRefRole._re_genex.match(title)
262                     if m:
263                         title = m.group(1)
264                 targetname = 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)
275
276 class CMakeObject(ObjectDescription):
277
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)
283             if m:
284                 sig = m.group(1)
285         return sig
286
287     def add_target_and_index(self, name, sig, signode):
288         if self.objtype == 'command':
289            targetname = name.lower()
290         else:
291            targetname = name
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)
300
301         make_index_entry = _cmake_index_objs.get(self.objtype)
302         if make_index_entry:
303             self.indexnode['entries'].append(make_index_entry(name, targetid))
304
305 class CMakeXRefRole(XRefRole):
306
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)
312
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)
320             if m:
321                 text = '%s <%s>' % (text, m.group(1))
322         elif typ == 'cmake:genex':
323             m = CMakeXRefRole._re_genex.match(text)
324             if m:
325                 text = '%s <%s>' % (text, m.group(1))
326         elif typ == 'cmake:guide':
327             m = CMakeXRefRole._re_guide.match(text)
328             if m:
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.
332         while True:
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))
336             else:
337                 break
338         return XRefRole.__call__(self, typ, rawtext, text, *args, **keys)
339
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.
347     #
348     # def result_nodes(self, document, env, node, is_ref):
349     #     pass
350
351 class CMakeXRefTransform(Transform):
352
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
357
358     def apply(self):
359         env = self.document.settings.env
360
361         # Find CMake cross-reference nodes and add index and target
362         # nodes for them.
363         for ref in self.document.traverse(addnodes.pending_xref):
364             if not ref['refdomain'] == 'cmake':
365                 continue
366
367             objtype = ref['reftype']
368             make_index_entry = _cmake_index_objs.get(objtype)
369             if not make_index_entry:
370                 continue
371
372             objname = ref['reftarget']
373             if objtype == 'guide' and CMakeXRefRole._re_guide.match(objname):
374                 # Do not index cross-references to guide sections.
375                 continue
376
377             targetnum = env.new_serialno('index-%s:%s' % (objtype, objname))
378
379             targetid = 'index-%s-%s:%s' % (targetnum, objtype, objname)
380             targetnode = nodes.target('', '', ids=[targetid])
381             self.document.note_explicit_target(targetnode)
382
383             indexnode = addnodes.index()
384             indexnode['entries'] = [make_index_entry(objname, targetid, '')]
385             ref.replace_self([indexnode, targetnode, ref])
386
387 class CMakeDomain(Domain):
388     """CMake domain."""
389     name = 'cmake'
390     label = 'CMake'
391     object_types = {
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'),
409     }
410     directives = {
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,
427     }
428     roles = {
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(),
446     }
447     initial_data = {
448         'objects': {},  # fullname -> docname, objtype
449     }
450
451     def clear_doc(self, docname):
452         to_clear = set()
453         for fullname, (fn, _) in self.data['objects'].items():
454             if fn == docname:
455                 to_clear.add(fullname)
456         for fullname in to_clear:
457             del self.data['objects'][fullname]
458
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)
463         if obj is None:
464             # TODO: warn somehow?
465             return None
466         return make_refnode(builder, fromdocname, obj[0], targetid,
467                             contnode, target)
468
469     def get_objects(self):
470         for refname, (docname, type) in self.data['objects'].items():
471             yield (refname, refname, type, docname, refname, 1)
472
473 def setup(app):
474     app.add_directive('cmake-module', CMakeModule)
475     app.add_transform(CMakeTransform)
476     app.add_transform(CMakeXRefTransform)
477     app.add_domain(CMakeDomain)