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