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