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
77__docformat__ = 'reStructuredText'
78
79import warnings
80
81from docutils import nodes
82from docutils.parsers.rst import directives
83from docutils.parsers.rst.languages import en as _fallback_language_module
84from docutils.utils.code_analyzer import Lexer, LexerError
85
86DEFAULT_INTERPRETED_ROLE = 'title-reference'
87"""The canonical name of the default interpreted role.
88
89This role is used when no role is specified for a piece of interpreted text.
90"""
91
92_role_registry = {}
93"""Mapping of canonical role names to role functions.
94
95Language-dependent role names are defined in the ``language`` subpackage.
96"""
97
98_roles = {}
99"""Mapping of local or language-dependent interpreted text role names to role
100functions."""
101
102
103def role(role_name, language_module, lineno, reporter):
104 """
105 Locate and return a role function from its language-dependent name, along
106 with a list of system messages.
107
108 If the role is not found in the current language, check English. Return a
109 2-tuple: role function (``None`` if the named role cannot be found) and a
110 list of system messages.
111 """
112 normname = role_name.lower()
113 messages = []
114 msg_text = []
115
116 if normname in _roles:
117 return _roles[normname], messages
118
119 if role_name:
120 canonicalname = None
121 try:
122 canonicalname = language_module.roles[normname]
123 except AttributeError as error:
124 msg_text.append('Problem retrieving role entry from language '
125 'module %r: %s.' % (language_module, error))
126 except KeyError:
127 msg_text.append('No role entry for "%s" in module "%s".'
128 % (role_name, language_module.__name__))
129 else:
130 canonicalname = DEFAULT_INTERPRETED_ROLE
131
132 # If we didn't find it, try English as a fallback.
133 if not canonicalname:
134 try:
135 canonicalname = _fallback_language_module.roles[normname]
136 msg_text.append('Using English fallback for role "%s".'
137 % role_name)
138 except KeyError:
139 msg_text.append('Trying "%s" as canonical role name.'
140 % role_name)
141 # The canonical name should be an English name, but just in case:
142 canonicalname = normname
143
144 # Collect any messages that we generated.
145 if msg_text:
146 message = reporter.info('\n'.join(msg_text), line=lineno)
147 messages.append(message)
148
149 # Look the role up in the registry, and return it.
150 if canonicalname in _role_registry:
151 role_fn = _role_registry[canonicalname]
152 register_local_role(normname, role_fn)
153 return role_fn, messages
154 return None, messages # Error message will be generated by caller.
155
156
157def register_canonical_role(name, role_fn) -> None:
158 """
159 Register an interpreted text role by its canonical name.
160
161 :Parameters:
162 - `name`: The canonical name of the interpreted role.
163 - `role_fn`: The role function. See the module docstring.
164 """
165 set_implicit_options(role_fn)
166 _role_registry[name.lower()] = role_fn
167
168
169def register_local_role(name, role_fn) -> None:
170 """
171 Register an interpreted text role by its local or language-dependent name.
172
173 :Parameters:
174 - `name`: The local or language-dependent name of the interpreted role.
175 - `role_fn`: The role function. See the module docstring.
176 """
177 set_implicit_options(role_fn)
178 _roles[name.lower()] = role_fn
179
180
181def set_implicit_options(role_fn) -> None:
182 """
183 Add customization options to role functions, unless explicitly set or
184 disabled.
185 """
186 if not hasattr(role_fn, 'options') or role_fn.options is None:
187 role_fn.options = {'class': directives.class_option}
188 elif 'class' not in role_fn.options:
189 role_fn.options['class'] = directives.class_option
190
191
192def register_generic_role(canonical_name, node_class) -> None:
193 """For roles which simply wrap a given `node_class` around the text."""
194 role = GenericRole(canonical_name, node_class)
195 register_canonical_role(canonical_name, role)
196
197
198class GenericRole:
199 """
200 Generic interpreted text role.
201
202 The interpreted text is simply wrapped with the provided node class.
203 """
204
205 def __init__(self, role_name, node_class) -> None:
206 self.name = role_name
207 self.node_class = node_class
208
209 def __call__(self, role, rawtext, text, lineno, inliner,
210 options=None, content=None):
211 options = normalize_options(options)
212 return [self.node_class(rawtext, text, **options)], []
213
214
215class CustomRole:
216 """Wrapper for custom interpreted text roles."""
217
218 def __init__(
219 self, role_name, base_role, options=None, content=None,
220 ) -> None:
221 self.name = role_name
222 self.base_role = base_role
223 self.options = getattr(base_role, 'options', None)
224 self.content = getattr(base_role, 'content', None)
225 self.supplied_options = options
226 self.supplied_content = content
227
228 def __call__(self, role, rawtext, text, lineno, inliner,
229 options=None, content=None):
230 opts = normalize_options(self.supplied_options)
231 try:
232 opts.update(options)
233 except TypeError: # options may be ``None``
234 pass
235 # pass concatenation of content from instance and call argument:
236 supplied_content = self.supplied_content or []
237 content = content or []
238 delimiter = ['\n'] if supplied_content and content else []
239 return self.base_role(role, rawtext, text, lineno, inliner,
240 options=opts,
241 content=supplied_content+delimiter+content)
242
243
244def generic_custom_role(role, rawtext, text, lineno, inliner,
245 options=None, content=None):
246 """Base for custom roles if no other base role is specified."""
247 # Once nested inline markup is implemented, this and other methods should
248 # recursively call inliner.nested_parse().
249 options = normalize_options(options)
250 return [nodes.inline(rawtext, text, **options)], []
251
252
253generic_custom_role.options = {'class': directives.class_option}
254
255
256######################################################################
257# Define and register the standard roles:
258######################################################################
259
260register_generic_role('abbreviation', nodes.abbreviation)
261register_generic_role('acronym', nodes.acronym)
262register_generic_role('emphasis', nodes.emphasis)
263register_generic_role('literal', nodes.literal)
264register_generic_role('strong', nodes.strong)
265register_generic_role('subscript', nodes.subscript)
266register_generic_role('superscript', nodes.superscript)
267register_generic_role('title-reference', nodes.title_reference)
268
269
270def pep_reference_role(role, rawtext, text, lineno, inliner,
271 options=None, content=None):
272 options = normalize_options(options)
273 try:
274 pepnum = int(nodes.unescape(text))
275 if pepnum < 0 or pepnum > 9999:
276 raise ValueError
277 except ValueError:
278 msg = inliner.reporter.error(
279 'PEP number must be a number from 0 to 9999; "%s" is invalid.'
280 % text, line=lineno)
281 prb = inliner.problematic(rawtext, rawtext, msg)
282 return [prb], [msg]
283 # Base URL mainly used by inliner.pep_reference; so this is correct:
284 ref = (inliner.document.settings.pep_base_url
285 + inliner.document.settings.pep_file_url_template % pepnum)
286 return [nodes.reference(rawtext, 'PEP ' + text, refuri=ref, **options)], []
287
288
289register_canonical_role('pep-reference', pep_reference_role)
290
291
292def rfc_reference_role(role, rawtext, text, lineno, inliner,
293 options=None, content=None):
294 options = normalize_options(options)
295 if "#" in text:
296 rfcnum, section = nodes.unescape(text).split("#", 1)
297 else:
298 rfcnum, section = nodes.unescape(text), None
299 try:
300 rfcnum = int(rfcnum)
301 if rfcnum < 1:
302 raise ValueError
303 except ValueError:
304 msg = inliner.reporter.error(
305 'RFC number must be a number greater than or equal to 1; '
306 '"%s" is invalid.' % text, line=lineno)
307 prb = inliner.problematic(rawtext, rawtext, msg)
308 return [prb], [msg]
309 # Base URL mainly used by inliner.rfc_reference, so this is correct:
310 ref = inliner.document.settings.rfc_base_url + inliner.rfc_url % rfcnum
311 if section is not None:
312 ref += "#" + section
313 node = nodes.reference(rawtext, 'RFC '+str(rfcnum), refuri=ref, **options)
314 return [node], []
315
316
317register_canonical_role('rfc-reference', rfc_reference_role)
318
319
320def raw_role(role, rawtext, text, lineno, inliner, options=None, content=None):
321 options = normalize_options(options)
322 if not inliner.document.settings.raw_enabled:
323 msg = inliner.reporter.warning('raw (and derived) roles disabled')
324 prb = inliner.problematic(rawtext, rawtext, msg)
325 return [prb], [msg]
326 if 'format' not in options:
327 msg = inliner.reporter.error(
328 'No format (Writer name) is associated with this role: "%s".\n'
329 'The "raw" role cannot be used directly.\n'
330 'Instead, use the "role" directive to create a new role with '
331 'an associated format.' % role, line=lineno)
332 prb = inliner.problematic(rawtext, rawtext, msg)
333 return [prb], [msg]
334 node = nodes.raw(rawtext, nodes.unescape(text, True), **options)
335 node.source, node.line = inliner.reporter.get_source_and_line(lineno)
336 return [node], []
337
338
339raw_role.options = {'format': directives.unchanged}
340
341register_canonical_role('raw', raw_role)
342
343
344def code_role(role, rawtext, text, lineno, inliner,
345 options=None, content=None):
346 options = normalize_options(options)
347 language = options.get('language', '')
348 classes = ['code']
349 if 'classes' in options:
350 classes.extend(options['classes'])
351 if language and language not in classes:
352 classes.append(language)
353 try:
354 tokens = Lexer(nodes.unescape(text, True), language,
355 inliner.document.settings.syntax_highlight)
356 except LexerError as error:
357 msg = inliner.reporter.warning(error)
358 prb = inliner.problematic(rawtext, rawtext, msg)
359 return [prb], [msg]
360
361 node = nodes.literal(rawtext, '', classes=classes)
362
363 # analyse content and add nodes for every token
364 for classes, value in tokens:
365 if classes:
366 node += nodes.inline(value, value, classes=classes)
367 else:
368 # insert as Text to decrease the verbosity of the output
369 node += nodes.Text(value)
370
371 return [node], []
372
373
374code_role.options = {'language': directives.unchanged}
375
376register_canonical_role('code', code_role)
377
378
379def math_role(role, rawtext, text, lineno, inliner,
380 options=None, content=None):
381 options = normalize_options(options)
382 text = nodes.unescape(text, True) # raw text without inline role markup
383 node = nodes.math(rawtext, text, **options)
384 return [node], []
385
386
387register_canonical_role('math', math_role)
388
389
390######################################################################
391# Register roles that are currently unimplemented.
392######################################################################
393
394def unimplemented_role(role, rawtext, text, lineno, inliner,
395 options=None, content=None):
396 msg = inliner.reporter.error(
397 'Interpreted text role "%s" not implemented.' % role, line=lineno)
398 prb = inliner.problematic(rawtext, rawtext, msg)
399 return [prb], [msg]
400
401
402register_canonical_role('index', unimplemented_role)
403register_canonical_role('named-reference', unimplemented_role)
404register_canonical_role('anonymous-reference', unimplemented_role)
405register_canonical_role('uri-reference', unimplemented_role)
406register_canonical_role('footnote-reference', unimplemented_role)
407register_canonical_role('citation-reference', unimplemented_role)
408register_canonical_role('substitution-reference', unimplemented_role)
409register_canonical_role('target', unimplemented_role)
410
411# This should remain unimplemented, for testing purposes:
412register_canonical_role('restructuredtext-unimplemented-role',
413 unimplemented_role)
414
415
416def set_classes(options) -> None:
417 """Deprecated. Obsoleted by ``normalize_options()``."""
418 warnings.warn('The auxiliary function roles.set_classes() is obsoleted'
419 ' by roles.normalize_options() and will be removed'
420 ' in Docutils 1.0', DeprecationWarning, stacklevel=2)
421 if options and 'class' in options:
422 assert 'classes' not in options
423 options['classes'] = options['class']
424 del options['class']
425
426
427def normalized_role_options(options):
428 warnings.warn('The auxiliary function roles.normalized_role_options() is '
429 'obsoleted by roles.normalize_options() and will be removed'
430 ' in Docutils 1.0', DeprecationWarning, stacklevel=2)
431 return normalize_options(options)
432
433
434def normalize_options(options):
435 """
436 Return normalized dictionary of role/directive options.
437
438 * ``None`` is replaced by an empty dictionary.
439 * The key 'class' is renamed to 'classes'.
440 """
441 if options is None:
442 return {}
443 n_options = options.copy()
444 if 'class' in n_options:
445 assert 'classes' not in n_options
446 n_options['classes'] = n_options['class']
447 del n_options['class']
448 return n_options