1# $Id$
2# Author: Edward Loper <edloper@gradient.cis.upenn.edu>
3# Copyright: This module has been placed in the public domain.
4
5"""
6This module defines standard interpreted text role functions, a registry for
7interpreted text roles, and an API for adding to and retrieving from the
8registry. See also `Creating reStructuredText Interpreted Text Roles`__.
9
10__ https://docutils.sourceforge.io/docs/ref/rst/roles.html
11
12
13The interface for interpreted role functions is as follows::
14
15 def role_fn(name, rawtext, text, lineno, inliner,
16 options=None, content=None):
17 code...
18
19 # Set function attributes for customization:
20 role_fn.options = ...
21 role_fn.content = ...
22
23Parameters:
24
25- ``name`` is the local name of the interpreted text role, the role name
26 actually used in the document.
27
28- ``rawtext`` is a string containing the entire interpreted text construct.
29 Return it as a ``problematic`` node linked to a system message if there is a
30 problem.
31
32- ``text`` is the interpreted text content, with backslash escapes converted
33 to nulls (``\x00``).
34
35- ``lineno`` is the line number where the text block containing the
36 interpreted text begins.
37
38- ``inliner`` is the Inliner object that called the role function.
39 It defines the following useful attributes: ``reporter``,
40 ``problematic``, ``memo``, ``parent``, ``document``.
41
42- ``options``: A dictionary of directive options for customization, to be
43 interpreted by the role function. Used for additional attributes for the
44 generated elements and other functionality.
45
46- ``content``: A list of strings, the directive content for customization
47 ("role" directive). To be interpreted by the role function.
48
49Function attributes for customization, interpreted by the "role" directive:
50
51- ``options``: A dictionary, mapping known option names to conversion
52 functions such as `int` or `float`. ``None`` or an empty dict implies no
53 options to parse. Several directive option conversion functions are defined
54 in the `directives` module.
55
56 All role functions implicitly support the "class" option, unless disabled
57 with an explicit ``{'class': None}``.
58
59- ``content``: A boolean; true if content is allowed. Client code must handle
60 the case where content is required but not supplied (an empty content list
61 will be supplied).
62
63Note that unlike directives, the "arguments" function attribute is not
64supported for role customization. Directive arguments are handled by the
65"role" directive itself.
66
67Interpreted role functions return a tuple of two values:
68
69- A list of nodes which will be inserted into the document tree at the
70 point where the interpreted role was encountered (can be an empty
71 list).
72
73- A list of system messages, which will be inserted into the document tree
74 immediately after the end of the current inline block (can also be empty).
75"""
76
77from __future__ import annotations
78
79__docformat__ = 'reStructuredText'
80
81import warnings
82
83from docutils import nodes
84from docutils.parsers.rst import directives
85from docutils.parsers.rst.languages import en as _fallback_language_module
86from docutils.utils.code_analyzer import Lexer, LexerError
87
88DEFAULT_INTERPRETED_ROLE = 'title-reference'
89"""The canonical name of the default interpreted role.
90
91This role is used when no role is specified for a piece of interpreted text.
92"""
93
94_role_registry = {}
95"""Mapping of canonical role names to role functions.
96
97Language-dependent role names are defined in the ``language`` subpackage.
98"""
99
100_roles = {}
101"""Mapping of local or language-dependent interpreted text role names to role
102functions."""
103
104
105def role(role_name, language_module, lineno, reporter):
106 """
107 Locate and return a role function from its language-dependent name, along
108 with a list of system messages.
109
110 If the role is not found in the current language, check English. Return a
111 2-tuple: role function (``None`` if the named role cannot be found) and a
112 list of system messages.
113 """
114 normname = role_name.lower()
115 messages = []
116 msg_text = []
117
118 if normname in _roles:
119 return _roles[normname], messages
120
121 if role_name:
122 canonicalname = None
123 try:
124 canonicalname = language_module.roles[normname]
125 except AttributeError as error:
126 msg_text.append('Problem retrieving role entry from language '
127 'module %r: %s.' % (language_module, error))
128 except KeyError:
129 msg_text.append('No role entry for "%s" in module "%s".'
130 % (role_name, language_module.__name__))
131 else:
132 canonicalname = DEFAULT_INTERPRETED_ROLE
133
134 # If we didn't find it, try English as a fallback.
135 if not canonicalname:
136 try:
137 canonicalname = _fallback_language_module.roles[normname]
138 msg_text.append('Using English fallback for role "%s".'
139 % role_name)
140 except KeyError:
141 msg_text.append('Trying "%s" as canonical role name.'
142 % role_name)
143 # The canonical name should be an English name, but just in case:
144 canonicalname = normname
145
146 # Collect any messages that we generated.
147 if msg_text:
148 message = reporter.info('\n'.join(msg_text), line=lineno)
149 messages.append(message)
150
151 # Look the role up in the registry, and return it.
152 if canonicalname in _role_registry:
153 role_fn = _role_registry[canonicalname]
154 register_local_role(normname, role_fn)
155 return role_fn, messages
156 return None, messages # Error message will be generated by caller.
157
158
159def register_canonical_role(name, role_fn) -> None:
160 """
161 Register an interpreted text role by its canonical name.
162
163 :Parameters:
164 - `name`: The canonical name of the interpreted role.
165 - `role_fn`: The role function. See the module docstring.
166 """
167 set_implicit_options(role_fn)
168 _role_registry[name.lower()] = role_fn
169
170
171def register_local_role(name, role_fn) -> None:
172 """
173 Register an interpreted text role by its local or language-dependent name.
174
175 :Parameters:
176 - `name`: The local or language-dependent name of the interpreted role.
177 - `role_fn`: The role function. See the module docstring.
178 """
179 set_implicit_options(role_fn)
180 _roles[name.lower()] = role_fn
181
182
183def set_implicit_options(role_fn) -> None:
184 """
185 Add customization options to role functions, unless explicitly set or
186 disabled.
187 """
188 if not hasattr(role_fn, 'options') or role_fn.options is None:
189 role_fn.options = {'class': directives.class_option}
190 elif 'class' not in role_fn.options:
191 role_fn.options['class'] = directives.class_option
192
193
194def register_generic_role(canonical_name, node_class) -> None:
195 """For roles which simply wrap a given `node_class` around the text."""
196 role = GenericRole(canonical_name, node_class)
197 register_canonical_role(canonical_name, role)
198
199
200class GenericRole:
201 """
202 Generic interpreted text role.
203
204 The interpreted text is simply wrapped with the provided node class.
205 """
206
207 def __init__(self, role_name, node_class) -> None:
208 self.name = role_name
209 self.node_class = node_class
210
211 def __call__(self, role, rawtext, text, lineno, inliner,
212 options=None, content=None):
213 options = normalize_options(options)
214 return [self.node_class(rawtext, text, **options)], []
215
216
217class CustomRole:
218 """Wrapper for custom interpreted text roles."""
219
220 def __init__(
221 self, role_name, base_role, options=None, content=None,
222 ) -> None:
223 self.name = role_name
224 self.base_role = base_role
225 self.options = getattr(base_role, 'options', None)
226 self.content = getattr(base_role, 'content', None)
227 self.supplied_options = options
228 self.supplied_content = content
229
230 def __call__(self, role, rawtext, text, lineno, inliner,
231 options=None, content=None):
232 opts = normalize_options(self.supplied_options)
233 try:
234 opts.update(options)
235 except TypeError: # options may be ``None``
236 pass
237 # pass concatenation of content from instance and call argument:
238 supplied_content = self.supplied_content or []
239 content = content or []
240 delimiter = ['\n'] if supplied_content and content else []
241 return self.base_role(role, rawtext, text, lineno, inliner,
242 options=opts,
243 content=supplied_content+delimiter+content)
244
245
246def generic_custom_role(role, rawtext, text, lineno, inliner,
247 options=None, content=None):
248 """Base for custom roles if no other base role is specified."""
249 # Once nested inline markup is implemented, this and other methods should
250 # recursively call inliner.nested_parse().
251 options = normalize_options(options)
252 return [nodes.inline(rawtext, text, **options)], []
253
254
255generic_custom_role.options = {'class': directives.class_option}
256
257
258######################################################################
259# Define and register the standard roles:
260######################################################################
261
262register_generic_role('abbreviation', nodes.abbreviation)
263register_generic_role('acronym', nodes.acronym)
264register_generic_role('emphasis', nodes.emphasis)
265register_generic_role('literal', nodes.literal)
266register_generic_role('strong', nodes.strong)
267register_generic_role('subscript', nodes.subscript)
268register_generic_role('superscript', nodes.superscript)
269register_generic_role('title-reference', nodes.title_reference)
270
271
272def pep_reference_role(role, rawtext, text, lineno, inliner,
273 options=None, content=None):
274 options = normalize_options(options)
275 try:
276 pepnum = int(nodes.unescape(text))
277 if pepnum < 0 or pepnum > 9999:
278 raise ValueError
279 except ValueError:
280 msg = inliner.reporter.error(
281 'PEP number must be a number from 0 to 9999; "%s" is invalid.'
282 % text, line=lineno)
283 prb = inliner.problematic(rawtext, rawtext, msg)
284 return [prb], [msg]
285 # Base URL mainly used by inliner.pep_reference; so this is correct:
286 ref = (inliner.document.settings.pep_base_url
287 + inliner.document.settings.pep_file_url_template % pepnum)
288 return [nodes.reference(rawtext, 'PEP ' + text, refuri=ref, **options)], []
289
290
291register_canonical_role('pep-reference', pep_reference_role)
292
293
294def rfc_reference_role(role, rawtext, text, lineno, inliner,
295 options=None, content=None):
296 options = normalize_options(options)
297 if "#" in text:
298 rfcnum, section = nodes.unescape(text).split("#", 1)
299 else:
300 rfcnum, section = nodes.unescape(text), None
301 try:
302 rfcnum = int(rfcnum)
303 if rfcnum < 1:
304 raise ValueError
305 except ValueError:
306 msg = inliner.reporter.error(
307 'RFC number must be a number greater than or equal to 1; '
308 '"%s" is invalid.' % text, line=lineno)
309 prb = inliner.problematic(rawtext, rawtext, msg)
310 return [prb], [msg]
311 # Base URL mainly used by inliner.rfc_reference, so this is correct:
312 ref = inliner.document.settings.rfc_base_url + inliner.rfc_url % rfcnum
313 if section is not None:
314 ref += "#" + section
315 node = nodes.reference(rawtext, 'RFC '+str(rfcnum), refuri=ref, **options)
316 return [node], []
317
318
319register_canonical_role('rfc-reference', rfc_reference_role)
320
321
322def raw_role(role, rawtext, text, lineno, inliner, options=None, content=None):
323 options = normalize_options(options)
324 if not inliner.document.settings.raw_enabled:
325 msg = inliner.reporter.warning('raw (and derived) roles disabled')
326 prb = inliner.problematic(rawtext, rawtext, msg)
327 return [prb], [msg]
328 if 'format' not in options:
329 msg = inliner.reporter.error(
330 'No format (Writer name) is associated with this role: "%s".\n'
331 'The "raw" role cannot be used directly.\n'
332 'Instead, use the "role" directive to create a new role with '
333 'an associated format.' % role, line=lineno)
334 prb = inliner.problematic(rawtext, rawtext, msg)
335 return [prb], [msg]
336 node = nodes.raw(rawtext, nodes.unescape(text, True), **options)
337 node.source, node.line = inliner.reporter.get_source_and_line(lineno)
338 return [node], []
339
340
341raw_role.options = {'format': directives.unchanged}
342
343register_canonical_role('raw', raw_role)
344
345
346def code_role(role, rawtext, text, lineno, inliner,
347 options=None, content=None):
348 options = normalize_options(options)
349 language = options.get('language', '')
350 classes = ['code']
351 if 'classes' in options:
352 classes.extend(options['classes'])
353 if language and language not in classes:
354 classes.append(language)
355 try:
356 tokens = Lexer(nodes.unescape(text, True), language,
357 inliner.document.settings.syntax_highlight)
358 except LexerError as error:
359 msg = inliner.reporter.warning(error)
360 prb = inliner.problematic(rawtext, rawtext, msg)
361 return [prb], [msg]
362
363 node = nodes.literal(rawtext, '', classes=classes)
364
365 # analyse content and add nodes for every token
366 for classes, value in tokens:
367 if classes:
368 node += nodes.inline(value, value, classes=classes)
369 else:
370 # insert as Text to decrease the verbosity of the output
371 node += nodes.Text(value)
372
373 return [node], []
374
375
376code_role.options = {'language': directives.unchanged}
377
378register_canonical_role('code', code_role)
379
380
381def math_role(role, rawtext, text, lineno, inliner,
382 options=None, content=None):
383 options = normalize_options(options)
384 text = nodes.unescape(text, True) # raw text without inline role markup
385 node = nodes.math(rawtext, text, **options)
386 return [node], []
387
388
389register_canonical_role('math', math_role)
390
391
392######################################################################
393# Register roles that are currently unimplemented.
394######################################################################
395
396def unimplemented_role(role, rawtext, text, lineno, inliner,
397 options=None, content=None):
398 msg = inliner.reporter.error(
399 'Interpreted text role "%s" not implemented.' % role, line=lineno)
400 prb = inliner.problematic(rawtext, rawtext, msg)
401 return [prb], [msg]
402
403
404register_canonical_role('index', unimplemented_role)
405register_canonical_role('named-reference', unimplemented_role)
406register_canonical_role('anonymous-reference', unimplemented_role)
407register_canonical_role('uri-reference', unimplemented_role)
408register_canonical_role('footnote-reference', unimplemented_role)
409register_canonical_role('citation-reference', unimplemented_role)
410register_canonical_role('substitution-reference', unimplemented_role)
411register_canonical_role('target', unimplemented_role)
412
413# This should remain unimplemented, for testing purposes:
414register_canonical_role('restructuredtext-unimplemented-role',
415 unimplemented_role)
416
417
418def set_classes(options) -> None:
419 """Deprecated. Obsoleted by ``normalize_options()``."""
420 warnings.warn('The auxiliary function roles.set_classes() is obsoleted'
421 ' by roles.normalize_options() and will be removed'
422 ' in Docutils 2.0', PendingDeprecationWarning, stacklevel=2)
423 if options and 'class' in options:
424 assert 'classes' not in options
425 options['classes'] = options['class']
426 del options['class']
427
428
429def normalized_role_options(options):
430 warnings.warn('The auxiliary function roles.normalized_role_options() is '
431 'obsoleted by roles.normalize_options() and will be removed'
432 ' in Docutils 2.0', PendingDeprecationWarning, stacklevel=2)
433 return normalize_options(options)
434
435
436def normalize_options(options):
437 """
438 Return normalized dictionary of role/directive options.
439
440 * ``None`` is replaced by an empty dictionary.
441 * The key 'class' is renamed to 'classes'.
442 """
443 if options is None:
444 return {}
445 n_options = options.copy()
446 if 'class' in n_options:
447 assert 'classes' not in n_options
448 n_options['classes'] = n_options['class']
449 del n_options['class']
450 return n_options