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