1# $Id: __init__.py 10397 2026-08-31 08:26:50Z milde $
2# Author: David Goodger <goodger@python.org>
3# Copyright: This module has been placed in the public domain.
4
5"""
6This package contains directive implementation modules.
7"""
8
9from __future__ import annotations
10
11__docformat__ = 'reStructuredText'
12
13import re
14import codecs
15from importlib import import_module
16
17from docutils import nodes, parsers
18from docutils.utils import split_escaped_whitespace, escape2null
19from docutils.parsers.rst.languages import en as _fallback_language_module
20
21TYPE_CHECKING = False
22if TYPE_CHECKING:
23 from collections.abc import Callable, Container, Sequence
24 from typing import Any
25 from docutils.parsers.rst.languages import RSTLanguageModule
26
27
28_directive_registry = {
29 'attention': ('admonitions', 'Attention'),
30 'caution': ('admonitions', 'Caution'),
31 'code': ('body', 'CodeBlock'),
32 'danger': ('admonitions', 'Danger'),
33 'error': ('admonitions', 'Error'),
34 'important': ('admonitions', 'Important'),
35 'note': ('admonitions', 'Note'),
36 'tip': ('admonitions', 'Tip'),
37 'hint': ('admonitions', 'Hint'),
38 'warning': ('admonitions', 'Warning'),
39 'admonition': ('admonitions', 'Admonition'),
40 'sidebar': ('body', 'Sidebar'),
41 'topic': ('body', 'Topic'),
42 'line-block': ('body', 'LineBlock'),
43 'parsed-literal': ('body', 'ParsedLiteral'),
44 'math': ('body', 'MathBlock'),
45 'rubric': ('body', 'Rubric'),
46 'epigraph': ('body', 'Epigraph'),
47 'highlights': ('body', 'Highlights'),
48 'pull-quote': ('body', 'PullQuote'),
49 'compound': ('body', 'Compound'),
50 'container': ('body', 'Container'),
51 # 'questions': ('body', 'question_list'),
52 'table': ('tables', 'RSTTable'),
53 'csv-table': ('tables', 'CSVTable'),
54 'list-table': ('tables', 'ListTable'),
55 'image': ('images', 'Image'),
56 'figure': ('images', 'Figure'),
57 'contents': ('parts', 'Contents'),
58 'sectnum': ('parts', 'Sectnum'),
59 'header': ('parts', 'Header'),
60 'footer': ('parts', 'Footer'),
61 # 'footnotes': ('parts', 'footnotes'),
62 # 'citations': ('parts', 'citations'),
63 'target-notes': ('references', 'TargetNotes'),
64 'meta': ('misc', 'Meta'),
65 # 'imagemap': ('html', 'imagemap'),
66 'raw': ('misc', 'Raw'),
67 'include': ('misc', 'Include'),
68 'replace': ('misc', 'Replace'),
69 'unicode': ('misc', 'Unicode'),
70 'class': ('misc', 'Class'),
71 'role': ('misc', 'Role'),
72 'default-role': ('misc', 'DefaultRole'),
73 'title': ('misc', 'Title'),
74 'date': ('misc', 'Date'),
75 'restructuredtext-test-directive': ('misc', 'TestDirective'),
76 }
77"""Mapping of directive name to (module name, class name). The
78directive name is canonical & must be lowercase. Language-dependent
79names are defined in the ``language`` subpackage."""
80
81_directives = {}
82"""Cache of imported directives."""
83
84
85def directive(directive_name: str,
86 language_module: RSTLanguageModule,
87 document: nodes.document,
88 ) -> tuple[parsers.rst.Directive, list[nodes.system_message]]:
89 """
90 Locate and return a directive function from its language-dependent name.
91 If not found in the current language, check English. Return None if the
92 named directive cannot be found.
93 """
94 normname = directive_name.lower()
95 messages = []
96 msg_text = []
97 if normname in _directives:
98 return _directives[normname], messages
99 canonicalname = None
100 try:
101 canonicalname = language_module.directives[normname]
102 except AttributeError as error:
103 msg_text.append('Problem retrieving directive entry from language '
104 'module %r: %s.' % (language_module, error))
105 except KeyError:
106 msg_text.append('No directive entry for "%s" in module "%s".'
107 % (directive_name, language_module.__name__))
108 if not canonicalname:
109 try:
110 canonicalname = _fallback_language_module.directives[normname]
111 msg_text.append('Using English fallback for directive "%s".'
112 % directive_name)
113 except KeyError:
114 msg_text.append('Trying "%s" as canonical directive name.'
115 % directive_name)
116 # The canonical name should be an English name, but just in case:
117 canonicalname = normname
118 if msg_text:
119 message = document.reporter.info(
120 '\n'.join(msg_text), line=document.current_line)
121 messages.append(message)
122 try:
123 modulename, classname = _directive_registry[canonicalname]
124 except KeyError:
125 # Error handling done by caller.
126 return None, messages
127 try:
128 module = import_module('docutils.parsers.rst.directives.'+modulename)
129 except ImportError as detail:
130 messages.append(document.reporter.error(
131 'Error importing directive module "%s" (directive "%s"):\n%s'
132 % (modulename, directive_name, detail),
133 line=document.current_line))
134 return None, messages
135 try:
136 directive = getattr(module, classname)
137 _directives[normname] = directive
138 except AttributeError:
139 messages.append(document.reporter.error(
140 'No directive class "%s" in module "%s" (directive "%s").'
141 % (classname, modulename, directive_name),
142 line=document.current_line))
143 return None, messages
144 return directive, messages
145
146
147def register_directive(name: str, directive: Callable) -> None:
148 """
149 Register a nonstandard application-defined directive function.
150 Language lookups are not needed for such functions.
151 """
152 _directives[name] = directive
153
154
155# conversion functions for `Directive.option_spec`
156# ------------------------------------------------
157#
158# see also `parsers.rst.Directive` in ../__init__.py.
159
160
161def flag(argument: str|None) -> None:
162 """
163 Check for a valid flag option (no argument) and return ``None``.
164 (Directive option conversion function.)
165
166 Raise ``ValueError`` if an argument is found.
167 """
168 if argument and argument.strip():
169 raise ValueError('no argument is allowed; "%s" supplied' % argument)
170 else:
171 return None
172
173
174def unchanged_required(argument: str|None) -> str:
175 """
176 Return the argument text, unchanged.
177
178 Directive option conversion function for options that require a value.
179
180 Raise ``ValueError`` if no argument is found.
181 """
182 if argument is None:
183 raise ValueError('argument required but none supplied')
184 else:
185 return argument # unchanged!
186
187
188def unchanged(argument: str|None) -> str:
189 """
190 Return the argument text, unchanged.
191 (Directive option conversion function.)
192
193 No argument implies empty string ("").
194 """
195 if argument is None:
196 return ''
197 else:
198 return argument # unchanged!
199
200
201def path(argument: str|None) -> str:
202 """
203 Return the path argument unwrapped (with newlines removed).
204 (Directive option conversion function.)
205
206 Raise ``ValueError`` if no argument is found.
207 """
208 if argument is None:
209 raise ValueError('argument required but none supplied')
210 else:
211 return ''.join(s.strip() for s in argument.splitlines())
212
213
214def uri(argument: str|None) -> str:
215 """
216 Return the URI argument with unescaped whitespace removed.
217 (Directive option conversion function.)
218
219 Raise ``ValueError`` if no argument is found.
220 """
221 if argument is None:
222 raise ValueError('argument required but none supplied')
223 else:
224 parts = split_escaped_whitespace(escape2null(argument))
225 return ' '.join(''.join(nodes.unescape(part).split())
226 for part in parts)
227
228
229def nonnegative_int(argument: str|int|None) -> int:
230 """
231 Check for a nonnegative integer argument; raise ``ValueError`` if not.
232 (Directive option conversion function.)
233 """
234 value = int(argument)
235 if value < 0:
236 raise ValueError('negative value; must be positive or zero')
237 return value
238
239
240def percentage(argument: str|int|None) -> int:
241 """
242 Check for an integer percentage value with optional percent sign.
243 (Directive option conversion function.)
244 """
245 try:
246 argument = argument.rstrip(' %')
247 except AttributeError:
248 pass
249 return nonnegative_int(argument)
250
251
252CSS3_LENGTH_UNITS = ('em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax',
253 'cm', 'mm', 'Q', 'in', 'pt', 'pc', 'px')
254"""Length units that are supported by the reStructuredText parser.
255
256Corresponds to the `length units in CSS3`__.
257
258__ https://www.w3.org/TR/css-values-3/#lengths
259"""
260
261
262def get_measure(argument: str|None, units: Container[str]) -> str:
263 """
264 Check for a positive argument of one of the `units`.
265
266 Return a normalized string of the form "<value><unit>"
267 (without space inbetween).
268
269 To be called from directive option conversion functions.
270 """
271 value, unit = nodes.parse_measure(argument)
272 if value < 0 or unit not in units:
273 raise ValueError(
274 'not a positive number or measure of one of the following units:\n'
275 + ', '.join(u for u in units if u))
276 return f'{value}{unit}'
277
278
279def length_or_unitless(argument: str|None) -> str:
280 return get_measure(argument, CSS3_LENGTH_UNITS + ('',))
281
282
283def length_or_percentage_or_unitless(argument: str|None,
284 default: str = '') -> str:
285 """
286 Return normalized string of a length or percentage unit.
287 (Directive option conversion function.)
288
289 Add <default> if there is no unit. Raise ValueError if the argument is not
290 a positive measure of one of the valid CSS units (or without unit).
291
292 >>> length_or_percentage_or_unitless('3 pt')
293 '3pt'
294 >>> length_or_percentage_or_unitless('3%', 'em')
295 '3%'
296 >>> length_or_percentage_or_unitless('3')
297 '3'
298 >>> length_or_percentage_or_unitless('3', 'px')
299 '3px'
300 """
301 try:
302 return get_measure(argument, CSS3_LENGTH_UNITS + ('%',))
303 except ValueError as error:
304 try:
305 return get_measure(argument, ['']) + default
306 except ValueError:
307 raise error
308
309
310def class_option(argument: str|None) -> list[str]:
311 """
312 Convert the argument into a list of ID-compatible strings and return it.
313 (Directive option conversion function.)
314
315 Raise ``ValueError`` if no argument is found.
316 """
317 if argument is None:
318 raise ValueError('argument required but none supplied')
319 names = argument.split()
320 class_names = []
321 for name in names:
322 class_name = nodes.make_id(name)
323 if not class_name:
324 raise ValueError('cannot make "%s" into a class name' % name)
325 class_names.append(class_name)
326 return class_names
327
328
329unicode_pattern = re.compile(
330 r'(?:0x|x|\\x|U\+?|\\u)([0-9a-f]+)$|&#x([0-9a-f]+);$', re.IGNORECASE)
331
332
333def unicode_code(code: str|None) -> str:
334 r"""
335 Convert a Unicode character code to a Unicode character.
336 (Directive option conversion function.)
337
338 Codes may be decimal numbers, hexadecimal numbers (prefixed by ``0x``,
339 ``x``, ``\x``, ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style
340 numeric character entities (e.g. ``☮``). Other text remains as-is.
341
342 Raise ValueError for illegal Unicode code values.
343 """
344 try:
345 if code.isdigit(): # decimal number
346 return chr(int(code))
347 else:
348 match = unicode_pattern.match(code)
349 if match: # hex number
350 value = match.group(1) or match.group(2)
351 return chr(int(value, 16))
352 else: # other text
353 return code
354 except OverflowError as detail:
355 raise ValueError('code too large (%s)' % detail)
356
357
358def single_char_or_unicode(argument: str|None) -> str:
359 """
360 A single character is returned as-is. Unicode character codes are
361 converted as in `unicode_code`. (Directive option conversion function.)
362 """
363 char = unicode_code(argument)
364 if len(char) > 1:
365 raise ValueError('%r invalid; must be a single character or '
366 'a Unicode code' % char)
367 return char
368
369
370def single_char_or_whitespace_or_unicode(argument: str|None) -> str:
371 """
372 As with `single_char_or_unicode`, but "tab" and "space" are also supported.
373 (Directive option conversion function.)
374 """
375 if argument == 'tab':
376 char = '\t'
377 elif argument == 'space':
378 char = ' '
379 else:
380 char = single_char_or_unicode(argument)
381 return char
382
383
384def positive_int(argument: str|None|int) -> int:
385 """
386 Converts the argument into an integer. Raises ValueError for negative,
387 zero, or non-integer values. (Directive option conversion function.)
388 """
389 value = int(argument)
390 if value < 1:
391 raise ValueError('negative or zero value; must be positive')
392 return value
393
394
395def positive_int_list(argument: str|None) -> list[int]:
396 """
397 Converts a space- or comma-separated list of values into a Python list
398 of integers.
399 (Directive option conversion function.)
400
401 Raises ValueError for non-positive-integer values.
402
403 Provisional. May be removed in Docutils 2.0 or later.
404 """
405 if ',' in argument:
406 entries = argument.split(',')
407 else:
408 entries = argument.split()
409 return [positive_int(entry) for entry in entries]
410
411
412def encoding(argument: str|None) -> str:
413 """
414 Verifies the encoding argument by lookup.
415 (Directive option conversion function.)
416
417 Raises ValueError for unknown encodings.
418 """
419 try:
420 codecs.lookup(argument)
421 except LookupError:
422 raise ValueError('unknown encoding: "%s"' % argument)
423 return argument
424
425
426def choice(argument: str|None, values: Sequence[str]) -> str:
427 """
428 Directive option utility function, supplied to enable options whose
429 argument must be a member of a finite set of possible values (must be
430 lower case). A custom conversion function must be written to use it. For
431 example::
432
433 from docutils.parsers.rst import directives
434
435 def yesno(argument: str):
436 return directives.choice(argument, ('yes', 'no'))
437
438 Raise ``ValueError`` if no argument is found or if the argument's value is
439 not valid (not an entry in the supplied list).
440 """
441 try:
442 value = argument.lower().strip()
443 except AttributeError:
444 raise ValueError('must supply an argument; choose from %s'
445 % format_values(values))
446 if value in values:
447 return value
448 else:
449 raise ValueError('"%s" unknown; choose from %s'
450 % (argument, format_values(values)))
451
452
453def format_values(values: Sequence[str]) -> str:
454 return '%s, or "%s"' % (', '.join('"%s"' % s for s in values[:-1]),
455 values[-1])
456
457
458def value_or(values: Container[str], other: Callable) -> Callable:
459 """
460 Directive option conversion function.
461
462 The argument can be any of `values` or a value compatible with the
463 directive option conversion function `other`.
464 """
465 def one_or_other(argument: str|None) -> Any:
466 if argument in values:
467 return argument
468 else:
469 return other(argument)
470 return one_or_other
471
472
473def parser_name(argument: str|None) -> type[parsers.Parser]:
474 """
475 Return a docutils parser whose name matches the argument.
476 (Directive option conversion function.)
477
478 Return `None`, if the argument evaluates to `False`.
479 Raise `ValueError` if importing the parser module fails.
480 """
481 if not argument:
482 return None
483 try:
484 return parsers.get_parser_class(argument)
485 except ImportError as err:
486 raise ValueError(str(err))
487
488
489def column_widths(argument: str) -> list[str]:
490 """
491 Conversion function for the ``widths`` option of the table directives.
492
493 Converts string with a space- or comma-separated list of proportional
494 width values (with optional unit symbol "*") into a list of values.
495 Raises ValueError for non-positive and non-integer values.
496
497 Provisional.
498 See docs/ref/rst/directives.html#table-options
499 and docs/ref/doctree.html#colwidth.
500 """
501 # remove optional "proportional unit" symbol:
502 argument = argument.replace('*', '')
503 # extract values:
504 widths = positive_int_list(argument)
505 # return list of strings
506 return [f'{width}' for width in widths]