9215e1498631b8ca08cf3482ee72af87dca552c4
[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 try:
63     from docutils.utils.error_reporting import SafeString, ErrorString
64 except ImportError:
65     # error_reporting was not in utils before version 0.11:
66     from docutils.error_reporting import SafeString, ErrorString
67
68 from docutils import io, nodes
69
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
75
76 sphinx_before_1_4 = False
77 sphinx_before_1_7_2 = False
78 try:
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
84 except 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
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)
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
108 class 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
178 class _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
209 def _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
217 class 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
282 class 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
311 class 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
357 class 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
393 class 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
479 def setup(app):
480     app.add_directive('cmake-module', CMakeModule)
481     app.add_transform(CMakeTransform)
482     app.add_transform(CMakeXRefTransform)
483     app.add_domain(CMakeDomain)