1"""
2    sphinx.directives
3    ~~~~~~~~~~~~~~~~~
4
5    Handlers for additional ReST directives.
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11import re
12from typing import Any, Dict, Generic, List, Tuple, TypeVar, cast
13
14from docutils import nodes
15from docutils.nodes import Node
16from docutils.parsers.rst import directives, roles
17
18from sphinx import addnodes
19from sphinx.addnodes import desc_signature
20from sphinx.deprecation import (RemovedInSphinx40Warning, RemovedInSphinx50Warning,
21                                deprecated_alias)
22from sphinx.util import docutils
23from sphinx.util.docfields import DocFieldTransformer, Field, TypedField
24from sphinx.util.docutils import SphinxDirective
25from sphinx.util.typing import DirectiveOption
26
27if False:
28    # For type annotation
29    from sphinx.application import Sphinx
30
31
32# RE to strip backslash escapes
33nl_escape_re = re.compile(r'\\\n')
34strip_backslash_re = re.compile(r'\\(.)')
35
36T = TypeVar('T')
37
38
39def optional_int(argument: str) -> int:
40    """
41    Check for an integer argument or None value; raise ``ValueError`` if not.
42    """
43    if argument is None:
44        return None
45    else:
46        value = int(argument)
47        if value < 0:
48            raise ValueError('negative value; must be positive or zero')
49        return value
50
51
52class ObjectDescription(SphinxDirective, Generic[T]):
53    """
54    Directive to describe a class, function or similar object.  Not used
55    directly, but subclassed (in domain-specific directives) to add custom
56    behavior.
57    """
58
59    has_content = True
60    required_arguments = 1
61    optional_arguments = 0
62    final_argument_whitespace = True
63    option_spec = {
64        'noindex': directives.flag,
65    }  # type: Dict[str, DirectiveOption]
66
67    # types of doc fields that this directive handles, see sphinx.util.docfields
68    doc_field_types = []    # type: List[Field]
69    domain = None           # type: str
70    objtype = None          # type: str
71    indexnode = None        # type: addnodes.index
72
73    # Warning: this might be removed in future version. Don't touch this from extensions.
74    _doc_field_type_map = {}  # type: Dict[str, Tuple[Field, bool]]
75
76    def get_field_type_map(self) -> Dict[str, Tuple[Field, bool]]:
77        if self._doc_field_type_map == {}:
78            self._doc_field_type_map = {}
79            for field in self.doc_field_types:
80                for name in field.names:
81                    self._doc_field_type_map[name] = (field, False)
82
83                if field.is_typed:
84                    typed_field = cast(TypedField, field)
85                    for name in typed_field.typenames:
86                        self._doc_field_type_map[name] = (field, True)
87
88        return self._doc_field_type_map
89
90    def get_signatures(self) -> List[str]:
91        """
92        Retrieve the signatures to document from the directive arguments.  By
93        default, signatures are given as arguments, one per line.
94        """
95        lines = nl_escape_re.sub('', self.arguments[0]).split('\n')
96        if self.config.strip_signature_backslash:
97            # remove backslashes to support (dummy) escapes; helps Vim highlighting
98            return [strip_backslash_re.sub(r'\1', line.strip()) for line in lines]
99        else:
100            return [line.strip() for line in lines]
101
102    def handle_signature(self, sig: str, signode: desc_signature) -> T:
103        """
104        Parse the signature *sig* into individual nodes and append them to
105        *signode*. If ValueError is raised, parsing is aborted and the whole
106        *sig* is put into a single desc_name node.
107
108        The return value should be a value that identifies the object.  It is
109        passed to :meth:`add_target_and_index()` unchanged, and otherwise only
110        used to skip duplicates.
111        """
112        raise ValueError
113
114    def add_target_and_index(self, name: T, sig: str, signode: desc_signature) -> None:
115        """
116        Add cross-reference IDs and entries to self.indexnode, if applicable.
117
118        *name* is whatever :meth:`handle_signature()` returned.
119        """
120        return  # do nothing by default
121
122    def before_content(self) -> None:
123        """
124        Called before parsing content. Used to set information about the current
125        directive context on the build environment.
126        """
127        pass
128
129    def transform_content(self, contentnode: addnodes.desc_content) -> None:
130        """
131        Called after creating the content through nested parsing,
132        but before the ``object-description-transform`` event is emitted,
133        and before the info-fields are transformed.
134        Can be used to manipulate the content.
135        """
136        pass
137
138    def after_content(self) -> None:
139        """
140        Called after parsing content. Used to reset information about the
141        current directive context on the build environment.
142        """
143        pass
144
145    def run(self) -> List[Node]:
146        """
147        Main directive entry function, called by docutils upon encountering the
148        directive.
149
150        This directive is meant to be quite easily subclassable, so it delegates
151        to several additional methods.  What it does:
152
153        * find out if called as a domain-specific directive, set self.domain
154        * create a `desc` node to fit all description inside
155        * parse standard options, currently `noindex`
156        * create an index node if needed as self.indexnode
157        * parse all given signatures (as returned by self.get_signatures())
158          using self.handle_signature(), which should either return a name
159          or raise ValueError
160        * add index entries using self.add_target_and_index()
161        * parse the content and handle doc fields in it
162        """
163        if ':' in self.name:
164            self.domain, self.objtype = self.name.split(':', 1)
165        else:
166            self.domain, self.objtype = '', self.name
167        self.indexnode = addnodes.index(entries=[])
168
169        node = addnodes.desc()
170        node.document = self.state.document
171        node['domain'] = self.domain
172        # 'desctype' is a backwards compatible attribute
173        node['objtype'] = node['desctype'] = self.objtype
174        node['noindex'] = noindex = ('noindex' in self.options)
175        if self.domain:
176            node['classes'].append(self.domain)
177
178        self.names = []  # type: List[T]
179        signatures = self.get_signatures()
180        for i, sig in enumerate(signatures):
181            # add a signature node for each signature in the current unit
182            # and add a reference target for it
183            signode = addnodes.desc_signature(sig, '')
184            self.set_source_info(signode)
185            node.append(signode)
186            try:
187                # name can also be a tuple, e.g. (classname, objname);
188                # this is strictly domain-specific (i.e. no assumptions may
189                # be made in this base class)
190                name = self.handle_signature(sig, signode)
191            except ValueError:
192                # signature parsing failed
193                signode.clear()
194                signode += addnodes.desc_name(sig, sig)
195                continue  # we don't want an index entry here
196            if name not in self.names:
197                self.names.append(name)
198                if not noindex:
199                    # only add target and index entry if this is the first
200                    # description of the object with this name in this desc block
201                    self.add_target_and_index(name, sig, signode)
202
203        contentnode = addnodes.desc_content()
204        node.append(contentnode)
205        if self.names:
206            # needed for association of version{added,changed} directives
207            self.env.temp_data['object'] = self.names[0]
208        self.before_content()
209        self.state.nested_parse(self.content, self.content_offset, contentnode)
210        self.transform_content(contentnode)
211        self.env.app.emit('object-description-transform',
212                          self.domain, self.objtype, contentnode)
213        DocFieldTransformer(self).transform_all(contentnode)
214        self.env.temp_data['object'] = None
215        self.after_content()
216        return [self.indexnode, node]
217
218
219class DefaultRole(SphinxDirective):
220    """
221    Set the default interpreted text role.  Overridden from docutils.
222    """
223
224    optional_arguments = 1
225    final_argument_whitespace = False
226
227    def run(self) -> List[Node]:
228        if not self.arguments:
229            docutils.unregister_role('')
230            return []
231        role_name = self.arguments[0]
232        role, messages = roles.role(role_name, self.state_machine.language,
233                                    self.lineno, self.state.reporter)
234        if role:
235            docutils.register_role('', role)
236            self.env.temp_data['default_role'] = role_name
237        else:
238            literal_block = nodes.literal_block(self.block_text, self.block_text)
239            reporter = self.state.reporter
240            error = reporter.error('Unknown interpreted text role "%s".' % role_name,
241                                   literal_block, line=self.lineno)
242            messages += [error]
243
244        return cast(List[nodes.Node], messages)
245
246
247class DefaultDomain(SphinxDirective):
248    """
249    Directive to (re-)set the default domain for this source file.
250    """
251
252    has_content = False
253    required_arguments = 1
254    optional_arguments = 0
255    final_argument_whitespace = False
256    option_spec = {}  # type: Dict
257
258    def run(self) -> List[Node]:
259        domain_name = self.arguments[0].lower()
260        # if domain_name not in env.domains:
261        #     # try searching by label
262        #     for domain in env.domains.values():
263        #         if domain.label.lower() == domain_name:
264        #             domain_name = domain.name
265        #             break
266        self.env.temp_data['default_domain'] = self.env.domains.get(domain_name)
267        return []
268
269from sphinx.directives.code import CodeBlock, Highlight, LiteralInclude  # noqa
270from sphinx.directives.other import (Acks, Author, Centered, Class, HList, Include,  # noqa
271                                     Only, SeeAlso, TabularColumns, TocTree, VersionChange)
272from sphinx.directives.patches import Figure, Meta  # noqa
273from sphinx.domains.index import IndexDirective  # noqa
274
275deprecated_alias('sphinx.directives',
276                 {
277                     'Highlight': Highlight,
278                     'CodeBlock': CodeBlock,
279                     'LiteralInclude': LiteralInclude,
280                     'TocTree': TocTree,
281                     'Author': Author,
282                     'Index': IndexDirective,
283                     'VersionChange': VersionChange,
284                     'SeeAlso': SeeAlso,
285                     'TabularColumns': TabularColumns,
286                     'Centered': Centered,
287                     'Acks': Acks,
288                     'HList': HList,
289                     'Only': Only,
290                     'Include': Include,
291                     'Class': Class,
292                     'Figure': Figure,
293                     'Meta': Meta,
294                 },
295                 RemovedInSphinx40Warning,
296                 {
297                     'Highlight': 'sphinx.directives.code.Highlight',
298                     'CodeBlock': 'sphinx.directives.code.CodeBlock',
299                     'LiteralInclude': 'sphinx.directives.code.LiteralInclude',
300                     'TocTree': 'sphinx.directives.other.TocTree',
301                     'Author': 'sphinx.directives.other.Author',
302                     'Index': 'sphinx.directives.other.IndexDirective',
303                     'VersionChange': 'sphinx.directives.other.VersionChange',
304                     'SeeAlso': 'sphinx.directives.other.SeeAlso',
305                     'TabularColumns': 'sphinx.directives.other.TabularColumns',
306                     'Centered': 'sphinx.directives.other.Centered',
307                     'Acks': 'sphinx.directives.other.Acks',
308                     'HList': 'sphinx.directives.other.HList',
309                     'Only': 'sphinx.directives.other.Only',
310                     'Include': 'sphinx.directives.other.Include',
311                     'Class': 'sphinx.directives.other.Class',
312                     'Figure': 'sphinx.directives.patches.Figure',
313                     'Meta': 'sphinx.directives.patches.Meta',
314                 })
315
316deprecated_alias('sphinx.directives',
317                 {
318                     'DescDirective': ObjectDescription,
319                 },
320                 RemovedInSphinx50Warning,
321                 {
322                     'DescDirective': 'sphinx.directives.ObjectDescription',
323                 })
324
325
326def setup(app: "Sphinx") -> Dict[str, Any]:
327    app.add_config_value("strip_signature_backslash", False, 'env')
328    directives.register_directive('default-role', DefaultRole)
329    directives.register_directive('default-domain', DefaultDomain)
330    directives.register_directive('describe', ObjectDescription)
331    # new, more consistent, name
332    directives.register_directive('object', ObjectDescription)
333
334    app.add_event('object-description-transform')
335
336    return {
337        'version': 'builtin',
338        'parallel_read_safe': True,
339        'parallel_write_safe': True,
340    }
341