| # Distributed under the OSI-approved BSD 3-Clause License. See accompanying | |
| # file LICENSE.rst or https://cmake.org/licensing for details. | |
| # BEGIN imports | |
| import os | |
| import re | |
| from dataclasses import dataclass | |
| from typing import Any, List, Tuple, Type, cast | |
| import sphinx | |
| # The following imports may fail if we don't have Sphinx 2.x or later. | |
| if sphinx.version_info >= (2,): | |
| from docutils import io, nodes | |
| from docutils.statemachine import ViewList | |
| from docutils.nodes import Element, Node, TextElement, system_message | |
| from docutils.parsers.rst import Directive, directives | |
| from docutils.transforms import Transform | |
| from docutils.utils.code_analyzer import Lexer, LexerError | |
| from sphinx import addnodes | |
| from sphinx.directives import ObjectDescription, nl_escape_re | |
| from sphinx.domains import Domain, ObjType | |
| from sphinx.domains.changeset import VersionChange | |
| from sphinx.domains.changeset import versionlabels, versionlabel_classes | |
| from sphinx.domains.std import OptionXRefRole | |
| from sphinx.roles import XRefRole | |
| from sphinx.util import logging, ws_re | |
| from sphinx.util.docutils import ReferenceRole | |
| from sphinx.util.nodes import make_refnode | |
| else: | |
| # Sphinx 2.x is required. | |
| assert sphinx.version_info >= (2,) | |
| # END imports | |
| # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |
| # BEGIN sphinx tweaks | |
| # Adjust the 'std' domain regex used to parse options so that it recognizes | |
| # e.g. `-W<name>` as the option `-W` with a value `<name>`, rather than | |
| # treating the entire string as the option name. | |
| # | |
| # See also https://github.com/sphinx-doc/sphinx/issues/14323. | |
| sphinx.domains.std.option_desc_re = ( | |
| re.compile(r'((?:/|--|-|\+)?[^\s=<]+)(=?\s*.*)')) | |
| # END sphinx tweaks | |
| # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |
| # BEGIN pygments tweaks | |
| # Override much of pygments' CMakeLexer. | |
| # We need to parse CMake syntax definitions, not CMake code. | |
| # For hard test cases that use much of the syntax below, see | |
| # - module/FindPkgConfig.html | |
| # (with "glib-2.0>=2.10 gtk+-2.0" and similar) | |
| # - module/ExternalProject.html | |
| # (with http:// https:// git@; also has command options -E --build) | |
| # - manual/cmake-buildsystem.7.html | |
| # (with nested $<..>; relative and absolute paths, "::") | |
| from pygments.lexer import bygroups # noqa I100 | |
| from pygments.lexers import CMakeLexer | |
| from pygments.token import (Comment, Name, Number, Operator, Punctuation, | |
| String, Text, Whitespace) | |
| # Notes on regular expressions below: | |
| # - [\.\+-] are needed for string constants like gtk+-2.0 | |
| # - Unix paths are recognized by '/'; support for Windows paths may be added | |
| # if needed | |
| # - (\\.) allows for \-escapes (used in manual/cmake-language.7) | |
| # - $<..$<..$>..> nested occurrence in cmake-buildsystem | |
| # - Nested variable evaluations are only supported in a limited capacity. | |
| # Only one level of nesting is supported and at most one nested variable can | |
| # be present. | |
| CMakeLexer.tokens["root"] = [ | |
| # [[string]] | |
| (r'\[\[(?:\W[\w\W]*?)?\]\]', String.Multiline), | |
| (r'\[(?P<level>=+)\[[\w\W]*?\](?P=level)\]', String.Multiline), | |
| # fctn( | |
| (r'\b(\w+)([ \t]*)(\()', | |
| bygroups(Name.Function, Text, Name.Function), '#push'), | |
| (r'\(', Name.Function, '#push'), | |
| (r'\)', Name.Function, '#pop'), | |
| (r'\[', Punctuation, '#push'), | |
| (r'\]', Punctuation, '#pop'), | |
| (r'[|;,.=*\-]', Punctuation), | |
| # used in commands/source_group | |
| (r'\\\\', Punctuation), | |
| (r'[:]', Operator), | |
| # used in FindPkgConfig.cmake | |
| (r'[<>]=', Punctuation), | |
| # $<...> | |
| (r'\$<', Operator, '#push'), | |
| # <expr> | |
| (r'<[^<|]+?>(\w*\.\.\.)?', Name.Variable), | |
| # ${..} $ENV{..}, possibly nested | |
| (r'(\$\w*\{)([^\}\$]*)?(?:(\$\w*\{)([^\}]+?)(\}))?([^\}]*?)(\})', | |
| bygroups(Operator, Name.Tag, Operator, Name.Tag, Operator, Name.Tag, | |
| Operator)), | |
| # DATA{ ...} | |
| (r'([A-Z]+\{)(.+?)(\})', bygroups(Operator, Name.Tag, Operator)), | |
| # URL, git@, ... | |
| (r'[a-z]+(@|(://))((\\.)|[\w.+-:/\\])+', Name.Attribute), | |
| # absolute path | |
| (r'/\w[\w\.\+-/\\]*', Name.Attribute), | |
| (r'/', Name.Attribute), | |
| # relative path | |
| (r'\w[\w\.\+-]*/[\w.+-/\\]*', Name.Attribute), | |
| # initial A-Z, contains a-z | |
| (r'[A-Z]((\\.)|[\w.+-])*[a-z]((\\.)|[\w.+-])*', Name.Builtin), | |
| (r'@?[A-Z][A-Z0-9_]*', Name.Constant), | |
| (r'[a-z_]((\\;)|(\\ )|[\w.+-])*', Name.Builtin), | |
| (r'[0-9][0-9\.]*', Number), | |
| # "string" | |
| (r'(?s)"(\\"|[^"])*"', String), | |
| (r'\.\.\.', Name.Variable), | |
| # <..|..> is different from <expr> | |
| # TODO: <..|..> should be {..|..}; remove when all instances are converted? | |
| (r'<', Punctuation, '#push'), | |
| (r'>', Punctuation, '#pop'), | |
| # {..|..} | |
| (r'\{', Punctuation, '#push'), | |
| (r'\}', Punctuation, '#pop'), | |
| (r'\n', Whitespace), | |
| (r'[ \t]+', Whitespace), | |
| (r'#.*\n', Comment), | |
| # fallback, for debugging only | |
| # (r'[^<>\])\}\|$"# \t\n]+', Name.Exception), | |
| ] | |
| # END pygments tweaks | |
| # %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% | |
| logger = logging.getLogger(__name__) | |
| # RE to split multiple command signatures. | |
| sig_end_re = re.compile(r'(?<=[)])\n') | |
| def _parse_rst(state, text: str) -> list[Node]: | |
| node = nodes.Element() | |
| state.nested_parse(ViewList([text], source=''), 0, node) | |
| return node.children | |
| @dataclass | |
| class ObjectEntry: | |
| docname: str | |
| objtype: str | |
| node_id: str | |
| name: str | |
| class CMakeModule(Directive): | |
| required_arguments = 1 | |
| optional_arguments = 0 | |
| final_argument_whitespace = True | |
| option_spec = {'encoding': directives.encoding} | |
| def __init__(self, *args, **keys): | |
| self.re_start = re.compile(r'^#\[(?P<eq>=*)\[\.rst:$') | |
| Directive.__init__(self, *args, **keys) | |
| def run(self): | |
| settings = self.state.document.settings | |
| if not settings.file_insertion_enabled: | |
| raise self.warning(f'{self.name!r} directive disabled.') | |
| env = self.state.document.settings.env | |
| rel_path, path = env.relfn2path(self.arguments[0]) | |
| path = os.path.normpath(path) | |
| encoding = self.options.get('encoding', settings.input_encoding) | |
| e_handler = settings.input_encoding_error_handler | |
| try: | |
| settings.record_dependencies.add(path) | |
| f = io.FileInput(source_path=path, encoding=encoding, | |
| error_handler=e_handler) | |
| except UnicodeEncodeError: | |
| msg = (f'Problems with {self.name!r} directive path:\n' | |
| f'Cannot encode input file path {path!r} (wrong locale?).') | |
| raise self.severe(msg) | |
| except IOError as error: | |
| msg = f'Problems with {self.name!r} directive path:\n{error}.' | |
| raise self.severe(msg) | |
| raw_lines = f.read().splitlines() | |
| f.close() | |
| rst = None | |
| lines = [] | |
| for line in raw_lines: | |
| if rst is not None and rst != '#': | |
| # Bracket mode: check for end bracket | |
| pos = line.find(rst) | |
| if pos >= 0: | |
| if line[0] == '#': | |
| line = '' | |
| else: | |
| line = line[0:pos] | |
| rst = None | |
| else: | |
| # Line mode: check for .rst start (bracket or line) | |
| m = self.re_start.match(line) | |
| if m: | |
| rst = f']{m.group("eq")}]' | |
| line = '' | |
| elif line == '#.rst:': | |
| rst = '#' | |
| line = '' | |
| elif rst == '#': | |
| if line == '#' or line[:2] == '# ': | |
| line = line[2:] | |
| else: | |
| rst = None | |
| line = '' | |
| elif rst is None: | |
| line = '' | |
| lines.append(line) | |
| if rst is not None and rst != '#': | |
| raise self.warning(f'{self.name!r} found unclosed bracket ' | |
| f'"#[{rst[1:-1]}[.rst:" in {path!r}') | |
| self.state_machine.insert_input(lines, path) | |
| return [] | |
| class _cmake_index_entry: | |
| def __init__(self, desc): | |
| self.desc = desc | |
| def __call__(self, title, targetid, main='main'): | |
| return ('pair', f'{self.desc} ; {title}', targetid, main, None) | |
| _cmake_index_objs = { | |
| 'command': _cmake_index_entry('command'), | |
| 'cpack_gen': _cmake_index_entry('cpack generator'), | |
| 'diagnostic': _cmake_index_entry('diagnostic'), | |
| 'envvar': _cmake_index_entry('envvar'), | |
| 'generator': _cmake_index_entry('generator'), | |
| 'genex': _cmake_index_entry('genex'), | |
| 'guide': _cmake_index_entry('guide'), | |
| 'manual': _cmake_index_entry('manual'), | |
| 'module': _cmake_index_entry('module'), | |
| 'policy': _cmake_index_entry('policy'), | |
| 'prop_cache': _cmake_index_entry('cache property'), | |
| 'prop_dir': _cmake_index_entry('directory property'), | |
| 'prop_fs': _cmake_index_entry('file set property'), | |
| 'prop_gbl': _cmake_index_entry('global property'), | |
| 'prop_inst': _cmake_index_entry('installed file property'), | |
| 'prop_rule': _cmake_index_entry('rule property'), | |
| 'prop_sf': _cmake_index_entry('source file property'), | |
| 'prop_test': _cmake_index_entry('test property'), | |
| 'prop_tgt': _cmake_index_entry('target property'), | |
| 'variable': _cmake_index_entry('variable'), | |
| } | |
| class CMakeTransform(Transform): | |
| # Run this transform early since we insert nodes we want | |
| # treated as if they were written in the documents. | |
| default_priority = 210 | |
| def __init__(self, document, startnode): | |
| Transform.__init__(self, document, startnode) | |
| self.titles = {} | |
| def parse_title(self, docname): | |
| """Parse a document title as the first line starting in [A-Za-z0-9<$] | |
| or fall back to the document basename if no such line exists. | |
| The cmake --help-*-list commands also depend on this convention. | |
| Return the title or False if the document file does not exist. | |
| """ | |
| settings = self.document.settings | |
| env = settings.env | |
| title = self.titles.get(docname) | |
| if title is None: | |
| fname = os.path.join(env.srcdir, docname+'.rst') | |
| try: | |
| f = open(fname, 'r', encoding=settings.input_encoding) | |
| except IOError: | |
| title = False | |
| else: | |
| for line in f: | |
| if len(line) > 0 and (line[0].isalnum() or | |
| line[0] == '<' or line[0] == '$'): | |
| title = line.rstrip() | |
| break | |
| f.close() | |
| if title is None: | |
| title = os.path.basename(docname) | |
| self.titles[docname] = title | |
| return title | |
| def apply(self): | |
| env = self.document.settings.env | |
| # Treat some documents as cmake domain objects. | |
| objtype, sep, tail = env.docname.partition('/') | |
| make_index_entry = _cmake_index_objs.get(objtype) | |
| if make_index_entry: | |
| title = self.parse_title(env.docname) | |
| # Insert the object link target. | |
| if objtype == 'command': | |
| targetname = title.lower() | |
| elif objtype == 'guide' and not tail.endswith('/index'): | |
| targetname = tail | |
| else: | |
| if objtype == 'genex': | |
| m = CMakeXRefRole._re_genex.match(title) | |
| if m: | |
| title = m.group(1) | |
| targetname = title | |
| targetid = f'{objtype}:{targetname}' | |
| targetnode = nodes.target('', '', ids=[targetid]) | |
| self.document.note_explicit_target(targetnode) | |
| self.document.insert(0, targetnode) | |
| # Insert the object index entry. | |
| indexnode = addnodes.index() | |
| indexnode['entries'] = [make_index_entry(title, targetid)] | |
| self.document.insert(0, indexnode) | |
| # Add to cmake domain object inventory | |
| domain = cast(CMakeDomain, env.get_domain('cmake')) | |
| domain.note_object(objtype, targetname, targetid, targetid) | |
| class CMakeObject(ObjectDescription): | |
| def __init__(self, *args, **kwargs): | |
| self.targetname = None | |
| super().__init__(*args, **kwargs) | |
| def handle_signature(self, sig, signode): | |
| # called from sphinx.directives.ObjectDescription.run() | |
| signode += addnodes.desc_name(sig, sig) | |
| return sig | |
| def add_target_and_index(self, name, sig, signode): | |
| if self.objtype == 'command': | |
| targetname = name.lower() | |
| elif self.targetname: | |
| targetname = self.targetname | |
| else: | |
| targetname = name | |
| targetid = f'{self.objtype}:{targetname}' | |
| if targetid not in self.state.document.ids: | |
| signode['names'].append(targetid) | |
| signode['ids'].append(targetid) | |
| signode['first'] = (not self.names) | |
| self.state.document.note_explicit_target(signode) | |
| domain = cast(CMakeDomain, self.env.get_domain('cmake')) | |
| domain.note_object(self.objtype, targetname, targetid, targetid, | |
| location=signode) | |
| make_index_entry = _cmake_index_objs.get(self.objtype) | |
| if make_index_entry: | |
| self.indexnode['entries'].append(make_index_entry(name, targetid)) | |
| class CMakeGenexObject(CMakeObject): | |
| option_spec = { | |
| 'target': directives.unchanged, | |
| } | |
| def handle_signature(self, sig, signode): | |
| name = super().handle_signature(sig, signode) | |
| m = CMakeXRefRole._re_genex.match(sig) | |
| if m: | |
| name = m.group(1) | |
| return name | |
| def run(self): | |
| target = self.options.get('target') | |
| if target is not None: | |
| self.targetname = target | |
| return super().run() | |
| class CMakeSignatureObject(CMakeObject): | |
| object_type = 'signature' | |
| BREAK_ALL = 'all' | |
| BREAK_SMART = 'smart' | |
| BREAK_VERBATIM = 'verbatim' | |
| BREAK_CHOICES = {BREAK_ALL, BREAK_SMART, BREAK_VERBATIM} | |
| def break_option(argument): | |
| return directives.choice(argument, CMakeSignatureObject.BREAK_CHOICES) | |
| option_spec = { | |
| 'target': directives.unchanged, | |
| 'break': break_option, | |
| } | |
| def _break_signature_all(sig: str) -> str: | |
| return ws_re.sub(' ', sig) | |
| def _break_signature_verbatim(sig: str) -> str: | |
| lines = [ws_re.sub('\xa0', line.strip()) for line in sig.split('\n')] | |
| return ' '.join(lines) | |
| def _break_signature_smart(sig: str) -> str: | |
| tokens = [] | |
| for line in sig.split('\n'): | |
| token = '' | |
| delim = '' | |
| for c in line.strip(): | |
| if len(delim) == 0 and ws_re.match(c): | |
| if len(token): | |
| tokens.append(ws_re.sub('\xa0', token)) | |
| token = '' | |
| else: | |
| if c == '[': | |
| delim += ']' | |
| elif c == '<': | |
| delim += '>' | |
| elif len(delim) and c == delim[-1]: | |
| delim = delim[:-1] | |
| token += c | |
| if len(token): | |
| tokens.append(ws_re.sub('\xa0', token)) | |
| return ' '.join(tokens) | |
| def __init__(self, *args, **kwargs): | |
| self.targetnames = {} | |
| self.break_style = CMakeSignatureObject.BREAK_SMART | |
| super().__init__(*args, **kwargs) | |
| def get_signatures(self) -> List[str]: | |
| content = nl_escape_re.sub('', self.arguments[0]) | |
| lines = sig_end_re.split(content) | |
| if self.break_style == CMakeSignatureObject.BREAK_VERBATIM: | |
| fixup = CMakeSignatureObject._break_signature_verbatim | |
| elif self.break_style == CMakeSignatureObject.BREAK_SMART: | |
| fixup = CMakeSignatureObject._break_signature_smart | |
| else: | |
| fixup = CMakeSignatureObject._break_signature_all | |
| return [fixup(line.strip()) for line in lines] | |
| def handle_signature(self, sig, signode): | |
| language = 'cmake' | |
| classes = ['code', 'cmake', 'highlight'] | |
| node = addnodes.desc_name(sig, '', classes=classes) | |
| try: | |
| tokens = Lexer(sig, language, 'short') | |
| except LexerError as error: | |
| if self.state.document.settings.report_level > 2: | |
| # Silently insert without syntax highlighting. | |
| tokens = Lexer(sig, language, 'none') | |
| else: | |
| raise self.warning(error) | |
| for classes, value in tokens: | |
| if value == '\xa0': | |
| node += nodes.inline(value, value, classes=['nbsp']) | |
| elif classes: | |
| node += nodes.inline(value, value, classes=classes) | |
| else: | |
| node += nodes.Text(value) | |
| signode.clear() | |
| signode += node | |
| return sig | |
| def add_target_and_index(self, name, sig, signode): | |
| sig = sig.replace('\xa0', ' ') | |
| if sig in self.targetnames: | |
| sigargs = self.targetnames[sig] | |
| else: | |
| def extract_keywords(params): | |
| if params[-1].endswith(')'): | |
| params[-1] = params[-1][:-1] | |
| for p in params: | |
| if p[0].isalpha(): | |
| yield p | |
| else: | |
| return | |
| keywords = extract_keywords(sig.split('(')[1].split()) | |
| sigargs = ' '.join(keywords) | |
| targetname = sigargs.lower() | |
| targetid = nodes.make_id(targetname) | |
| if targetid not in self.state.document.ids: | |
| signode['names'].append(targetname) | |
| signode['ids'].append(targetid) | |
| signode['first'] = (not self.names) | |
| self.state.document.note_explicit_target(signode) | |
| # Register the signature as a command object. | |
| command = sig.split('(')[0].lower() | |
| refname = f'{command}({sigargs})' | |
| refid = f'command:{command}({targetname})' | |
| domain = cast(CMakeDomain, self.env.get_domain('cmake')) | |
| domain.note_object('command', name=refname, target_id=refid, | |
| node_id=targetid, location=signode) | |
| def run(self): | |
| self.break_style = CMakeSignatureObject.BREAK_ALL | |
| targets = self.options.get('target') | |
| if targets is not None: | |
| signatures = self.get_signatures() | |
| targets = [t.strip() for t in targets.split('\n')] | |
| for signature, target in zip(signatures, targets): | |
| self.targetnames[signature] = target | |
| self.break_style = ( | |
| self.options.get('break', CMakeSignatureObject.BREAK_SMART)) | |
| return super().run() | |
| class CMakeDiagnosticObject(CMakeObject): | |
| object_type = 'diagnostic' | |
| required_arguments = 0 | |
| optional_arguments = 0 | |
| DEFAULT_CHOICES = {'ignore', 'warn', 'error'} | |
| def default_option(argument): | |
| return directives.choice( | |
| argument, CMakeDiagnosticObject.DEFAULT_CHOICES) | |
| option_spec = { | |
| 'default': default_option, | |
| 'parent': directives.unchanged, | |
| } | |
| def __init__(self, *args, **kwargs): | |
| self.targetname = None | |
| super().__init__(*args, **kwargs) | |
| def _preset_name(self) -> str: | |
| sep = False | |
| out = '' | |
| for c in self.targetname[4:]: | |
| if c == '_': | |
| sep = True | |
| elif sep: | |
| out += c | |
| sep = False | |
| else: | |
| out += c.lower() | |
| return out | |
| def _build_field(self, name: str, content: str | list[Node]) -> Node: | |
| if type(content) is not list: | |
| content = _parse_rst(self.state,content) | |
| name_node = nodes.field_name(text=name) | |
| body_node = nodes.field_body('', *content) | |
| return nodes.field('', name_node, body_node) | |
| def _build_cli(self) -> list[Node]: | |
| cname = self.targetname[4:].lower().replace('_', '-') | |
| ctext = f'-W[no-][error=]{cname}' | |
| return _parse_rst(self.state,f':option:`{ctext} <cmake -W>`') | |
| def _build_preset_refs(self) -> list[Node]: | |
| p = self._preset_name() | |
| w = f':preset:`warnings.{p} <configurePresets.warnings.{p}>`' | |
| e = f':preset:`errors.{p} <configurePresets.errors.{p}>`' | |
| return _parse_rst(self.state,f'{w}, {e}') | |
| def run(self) -> list[Node]: | |
| self.domain, self.objtype = self.name.split(':', 1) | |
| doc = self.state.document | |
| self.targetname = doc.next_node(nodes.title).astext() | |
| default = self.options['default'].capitalize() | |
| parent = self.options.get('parent') | |
| headers = nodes.field_list() | |
| headers += self._build_field('Command Line', self._build_cli()) | |
| headers += self._build_field('Presets', self._build_preset_refs()) | |
| headers += self._build_field('Default', default) | |
| if parent: | |
| parentRef = _parse_rst(self.state,f':diagnostic:`{parent}`') | |
| headers += self._build_field('Parent', parentRef) | |
| node = nodes.Element() | |
| self.state.nested_parse(self.content, self.content_offset, node) | |
| return [headers] + node.children | |
| class CMakeReferenceRole: | |
| # See sphinx.util.nodes.explicit_title_re; \x00 escapes '<'. | |
| _re = re.compile(r'^(.+?)(\s*)(?<!\x00)<(.*?)>$', re.DOTALL) | |
| @staticmethod | |
| def _escape_angle_brackets(text: str) -> str: | |
| # CMake cross-reference targets frequently contain '<' so escape | |
| # any explicit `<target>` with '<' not preceded by whitespace. | |
| while True: | |
| m = CMakeReferenceRole._re.match(text) | |
| if m and len(m.group(2)) == 0: | |
| text = f'{m.group(1)}\x00<{m.group(3)}>' | |
| else: | |
| break | |
| return text | |
| def __class_getitem__(cls, parent: Any): | |
| class Class(parent): | |
| def __call__(self, name: str, rawtext: str, text: str, | |
| *args, **kwargs | |
| ) -> Tuple[List[Node], List[system_message]]: | |
| text = CMakeReferenceRole._escape_angle_brackets(text) | |
| return super().__call__(name, rawtext, text, *args, **kwargs) | |
| return Class | |
| class CMakeCRefRole(CMakeReferenceRole[ReferenceRole]): | |
| nodeclass: Type[Element] = nodes.reference | |
| innernodeclass: Type[TextElement] = nodes.literal | |
| classes: List[str] = ['cmake', 'literal'] | |
| def run(self) -> Tuple[List[Node], List[system_message]]: | |
| refnode = self.nodeclass(self.rawtext) | |
| self.set_source_info(refnode) | |
| refnode['refid'] = nodes.make_id(self.target) | |
| refnode += self.innernodeclass(self.rawtext, self.title, | |
| classes=self.classes) | |
| return [refnode], [] | |
| class CMakeXRefRole(CMakeReferenceRole[XRefRole]): | |
| _re_sub = re.compile(r'^([^()\s]+)\s*\(([^()]*)\)$', re.DOTALL) | |
| _re_genex = re.compile(r'^\$<([^<>:]+)(:[^<>]+)?>$', re.DOTALL) | |
| _re_guide = re.compile(r'^([^<>/]+)/([^<>]*)$', re.DOTALL) | |
| _re_explicit = re.compile(r'^([^<>]+)\s+<([^<>]+)>$', re.DOTALL) | |
| def __call__(self, typ, rawtext, text, *args, **kwargs): | |
| if typ == 'cmake:command': | |
| # Translate a CMake command cross-reference of the form: | |
| # `command_name(SUB_COMMAND)` | |
| # to be its own explicit target: | |
| # `command_name(SUB_COMMAND) <command_name(SUB_COMMAND)>` | |
| # so the XRefRole `fix_parens` option does not add more `()`. | |
| m = CMakeXRefRole._re_sub.match(text) | |
| if m: | |
| text = f'{text} <{text}>' | |
| elif typ == 'cmake:genex': | |
| m = CMakeXRefRole._re_genex.match(text) | |
| if m: | |
| text = f'{text} <{m.group(1)}>' | |
| elif typ == 'cmake:guide': | |
| m = CMakeXRefRole._re_guide.match(text) | |
| if m: | |
| text = f'{m.group(2)} <{text}>' | |
| elif typ == 'cmake:preset': | |
| m = CMakeXRefRole._re_explicit.match(text) | |
| if m: | |
| rawtext = f'{m.group(1)} <CMakePresets.{m.group(2)}>' | |
| text = f'{m.group(1)} <CMakePresets.{m.group(2)}>' | |
| else: | |
| text = f'CMakePresets.{text}' | |
| return super().__call__(typ, rawtext, text, *args, **kwargs) | |
| # We cannot insert index nodes using the result_nodes method | |
| # because CMakeXRefRole is processed before substitution_reference | |
| # nodes are evaluated so target nodes (with 'ids' fields) would be | |
| # duplicated in each evaluated substitution replacement. The | |
| # docutils substitution transform does not allow this. Instead we | |
| # use our own CMakeXRefTransform below to add index entries after | |
| # substitutions are completed. | |
| # | |
| # def result_nodes(self, document, env, node, is_ref): | |
| # pass | |
| class CMakeOptionXRefRole(OptionXRefRole): | |
| def __init__(self, command: str) -> None: | |
| self.command = command | |
| super().__init__() | |
| def __call__(self, typ, rawtext, text, *args, **kwargs): | |
| content = f'{text} <{self.command} {re.split(r"[ =]", text)[0]}>' | |
| return super().__call__('std:option', text, content, *args, **kwargs) | |
| class CMakeXRefTransform(Transform): | |
| # Run this transform early since we insert nodes we want | |
| # treated as if they were written in the documents, but | |
| # after the sphinx (210) and docutils (220) substitutions. | |
| default_priority = 221 | |
| # This helper supports docutils < 0.18, which is missing 'findall', | |
| # and docutils == 0.18.0, which is missing 'traverse'. | |
| def _document_findall_as_list(self, condition): | |
| if hasattr(self.document, 'findall'): | |
| # Fully iterate into a list so the caller can grow 'self.document' | |
| # while iterating. | |
| return list(self.document.findall(condition)) | |
| # Fallback to 'traverse' on old docutils, which returns a list. | |
| return self.document.traverse(condition) | |
| def apply(self): | |
| env = self.document.settings.env | |
| # Find CMake cross-reference nodes and add index and target | |
| # nodes for them. | |
| for ref in self._document_findall_as_list(addnodes.pending_xref): | |
| if not ref['refdomain'] == 'cmake': | |
| continue | |
| objtype = ref['reftype'] | |
| make_index_entry = _cmake_index_objs.get(objtype) | |
| if not make_index_entry: | |
| continue | |
| objname = ref['reftarget'] | |
| if objtype == 'guide' and CMakeXRefRole._re_guide.match(objname): | |
| # Do not index cross-references to guide sections. | |
| continue | |
| if objtype == 'command': | |
| # Index signature references to their parent command. | |
| objname = objname.split('(')[0].lower() | |
| targetnum = env.new_serialno(f'index-{objtype}:{objname}') | |
| targetid = f'index-{targetnum}-{objtype}:{objname}' | |
| targetnode = nodes.target('', '', ids=[targetid]) | |
| self.document.note_explicit_target(targetnode) | |
| indexnode = addnodes.index() | |
| indexnode['entries'] = [make_index_entry(objname, targetid, '')] | |
| ref.replace_self([indexnode, targetnode, ref]) | |
| class CMakeDomain(Domain): | |
| """CMake domain.""" | |
| name = 'cmake' | |
| label = 'CMake' | |
| object_types = { | |
| 'command': ObjType('command', 'command'), | |
| 'cpack_gen': ObjType('cpack_gen', 'cpack_gen'), | |
| 'diagnostic': ObjType('diagnostic', 'diagnostic'), | |
| 'envvar': ObjType('envvar', 'envvar'), | |
| 'generator': ObjType('generator', 'generator'), | |
| 'genex': ObjType('genex', 'genex'), | |
| 'guide': ObjType('guide', 'guide'), | |
| 'variable': ObjType('variable', 'variable'), | |
| 'module': ObjType('module', 'module'), | |
| 'policy': ObjType('policy', 'policy'), | |
| 'prop_cache': ObjType('prop_cache', 'prop_cache'), | |
| 'prop_dir': ObjType('prop_dir', 'prop_dir'), | |
| 'prop_fs': ObjType('prop_fs', 'prop_fs'), | |
| 'prop_gbl': ObjType('prop_gbl', 'prop_gbl'), | |
| 'prop_inst': ObjType('prop_inst', 'prop_inst'), | |
| 'prop_rule': ObjType('prop_rule', 'prop_rule'), | |
| 'prop_sf': ObjType('prop_sf', 'prop_sf'), | |
| 'prop_test': ObjType('prop_test', 'prop_test'), | |
| 'prop_tgt': ObjType('prop_tgt', 'prop_tgt'), | |
| 'manual': ObjType('manual', 'manual'), | |
| } | |
| directives = { | |
| 'command': CMakeObject, | |
| 'envvar': CMakeObject, | |
| 'diagnostic': CMakeDiagnosticObject, | |
| 'genex': CMakeGenexObject, | |
| 'signature': CMakeSignatureObject, | |
| 'variable': CMakeObject, | |
| # Other `object_types` cannot be created except by the `CMakeTransform` | |
| } | |
| roles = { | |
| # NOTE: When adding roles here, consider whether cmRST should also be | |
| # updated. | |
| # General CMake reference roles. | |
| 'cref': CMakeCRefRole(), | |
| 'command': CMakeXRefRole(fix_parens=True, lowercase=True), | |
| 'cpack_gen': CMakeXRefRole(), | |
| 'diagnostic': CMakeXRefRole(), | |
| 'envvar': CMakeXRefRole(), | |
| 'generator': CMakeXRefRole(), | |
| 'genex': CMakeXRefRole(), | |
| 'guide': CMakeXRefRole(), | |
| 'variable': CMakeXRefRole(), | |
| 'module': CMakeXRefRole(), | |
| 'policy': CMakeXRefRole(), | |
| 'prop_cache': CMakeXRefRole(), | |
| 'prop_dir': CMakeXRefRole(), | |
| 'prop_fs': CMakeXRefRole(), | |
| 'prop_gbl': CMakeXRefRole(), | |
| 'prop_inst': CMakeXRefRole(), | |
| 'prop_rule': CMakeXRefRole(), | |
| 'prop_sf': CMakeXRefRole(), | |
| 'prop_test': CMakeXRefRole(), | |
| 'prop_tgt': CMakeXRefRole(), | |
| 'manual': CMakeXRefRole(), | |
| 'preset': CMakeXRefRole(lowercase=True, warn_dangling=True), | |
| # Roles for program-specific command-line options without the program | |
| # name (which add the name to form the ref). | |
| 'cmake-option': CMakeOptionXRefRole('cmake'), | |
| 'cmake-build-option': CMakeOptionXRefRole('cmake--build'), | |
| 'cmake-install-option': CMakeOptionXRefRole('cmake--install'), | |
| 'cmake-workflow-option': CMakeOptionXRefRole('cmake--workflow'), | |
| 'cpack-option': CMakeOptionXRefRole('cpack'), | |
| 'ctest-dashboard-option': CMakeOptionXRefRole('ctest-dashboard'), | |
| 'ctest-option': CMakeOptionXRefRole('ctest'), | |
| } | |
| initial_data = { | |
| 'objects': {}, # fullname -> ObjectEntry | |
| } | |
| def clear_doc(self, docname): | |
| to_clear = set() | |
| for fullname, obj in self.data['objects'].items(): | |
| if obj.docname == docname: | |
| to_clear.add(fullname) | |
| for fullname in to_clear: | |
| del self.data['objects'][fullname] | |
| def merge_domaindata(self, docnames, otherdata): | |
| """Merge domaindata from the workers/chunks when they return. | |
| Called once per parallelization chunk. | |
| Only used when sphinx is run in parallel mode. | |
| :param docnames: a Set of the docnames that are part of the current | |
| chunk to merge | |
| :param otherdata: the partial data calculated by the current chunk | |
| """ | |
| for refname, obj in otherdata['objects'].items(): | |
| if obj.docname in docnames: | |
| self.data['objects'][refname] = obj | |
| def resolve_xref(self, env, fromdocname, builder, | |
| typ, target, node, contnode): | |
| targetid = f'{typ}:{target}' | |
| obj = self.data['objects'].get(targetid) | |
| if obj is None: | |
| if typ == 'command': | |
| # If 'command(args)' wasn't found, try just 'command'. | |
| # TODO: remove this fallback? warn? | |
| # logger.warning(f'no match for {targetid}') | |
| command = target.split('(')[0] | |
| targetid = f'{typ}:{command}' | |
| obj = self.data['objects'].get(targetid) | |
| elif typ == 'preset': | |
| # Preset references are really just references to plain old | |
| # explicit targets. | |
| labels = env.get_domain('std').labels | |
| docname, labelid, sectname = labels.get(target, ('', '', '')) | |
| if not docname: | |
| return None | |
| if node['refexplicit']: | |
| sectname = node.astext() | |
| return make_refnode(builder, fromdocname, docname, labelid, | |
| nodes.literal('', sectname), target) | |
| if obj is None: | |
| # TODO: warn somehow? | |
| return None | |
| return make_refnode(builder, fromdocname, obj.docname, obj.node_id, | |
| contnode, target) | |
| def note_object(self, objtype: str, name: str, target_id: str, | |
| node_id: str, location: Any = None): | |
| if target_id in self.data['objects']: | |
| other = self.data['objects'][target_id].docname | |
| logger.warning( | |
| f'CMake object {target_id!r} also described in {other!r}', | |
| location=location) | |
| self.data['objects'][target_id] = ObjectEntry( | |
| self.env.docname, objtype, node_id, name) | |
| def get_objects(self): | |
| for refname, obj in self.data['objects'].items(): | |
| yield (refname, refname, obj.objtype, obj.docname, obj.node_id, 1) | |
| def setup(app): | |
| app.add_directive('cmake-module', CMakeModule) | |
| app.add_transform(CMakeTransform) | |
| app.add_transform(CMakeXRefTransform) | |
| app.add_domain(CMakeDomain) | |
| version_directives = { | |
| 'cmakefiles': 'cmakeFiles', | |
| 'codemodel': 'codemodel', | |
| 'presets': 'presets', | |
| 'toolchains': 'toolchains', | |
| } | |
| for directive, name in version_directives.items(): | |
| versionlabels.update({ | |
| f'{directive}-versionadded': f'Added in {name} version %s', | |
| f'{directive}-versionchanged': f'Changed in {name} version %s', | |
| f'{directive}-versionremoved': f'Removed in {name} version %s', | |
| }) | |
| versionlabel_classes.update({ | |
| f'{directive}-versionadded': 'added', | |
| f'{directive}-versionchanged': 'changed', | |
| f'{directive}-versionremoved': 'removed', | |
| }) | |
| app.add_directive(f'{directive}-versionadded', VersionChange) | |
| app.add_directive(f'{directive}-versionchanged', VersionChange) | |
| app.add_directive(f'{directive}-versionremoved', VersionChange) | |
| return {"parallel_read_safe": True} |