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