Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/docutils/parsers/rst/directives/__init__.py: 23%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

183 statements  

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 

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, Sequence 

24 

25 

26_directive_registry = { 

27 'attention': ('admonitions', 'Attention'), 

28 'caution': ('admonitions', 'Caution'), 

29 'code': ('body', 'CodeBlock'), 

30 'danger': ('admonitions', 'Danger'), 

31 'error': ('admonitions', 'Error'), 

32 'important': ('admonitions', 'Important'), 

33 'note': ('admonitions', 'Note'), 

34 'tip': ('admonitions', 'Tip'), 

35 'hint': ('admonitions', 'Hint'), 

36 'warning': ('admonitions', 'Warning'), 

37 'admonition': ('admonitions', 'Admonition'), 

38 'sidebar': ('body', 'Sidebar'), 

39 'topic': ('body', 'Topic'), 

40 'line-block': ('body', 'LineBlock'), 

41 'parsed-literal': ('body', 'ParsedLiteral'), 

42 'math': ('body', 'MathBlock'), 

43 'rubric': ('body', 'Rubric'), 

44 'epigraph': ('body', 'Epigraph'), 

45 'highlights': ('body', 'Highlights'), 

46 'pull-quote': ('body', 'PullQuote'), 

47 'compound': ('body', 'Compound'), 

48 'container': ('body', 'Container'), 

49 # 'questions': ('body', 'question_list'), 

50 'table': ('tables', 'RSTTable'), 

51 'csv-table': ('tables', 'CSVTable'), 

52 'list-table': ('tables', 'ListTable'), 

53 'image': ('images', 'Image'), 

54 'figure': ('images', 'Figure'), 

55 'contents': ('parts', 'Contents'), 

56 'sectnum': ('parts', 'Sectnum'), 

57 'header': ('parts', 'Header'), 

58 'footer': ('parts', 'Footer'), 

59 # 'footnotes': ('parts', 'footnotes'), 

60 # 'citations': ('parts', 'citations'), 

61 'target-notes': ('references', 'TargetNotes'), 

62 'meta': ('misc', 'Meta'), 

63 # 'imagemap': ('html', 'imagemap'), 

64 'raw': ('misc', 'Raw'), 

65 'include': ('misc', 'Include'), 

66 'replace': ('misc', 'Replace'), 

67 'unicode': ('misc', 'Unicode'), 

68 'class': ('misc', 'Class'), 

69 'role': ('misc', 'Role'), 

70 'default-role': ('misc', 'DefaultRole'), 

71 'title': ('misc', 'Title'), 

72 'date': ('misc', 'Date'), 

73 'restructuredtext-test-directive': ('misc', 'TestDirective'), 

74 } 

75"""Mapping of directive name to (module name, class name). The 

76directive name is canonical & must be lowercase. Language-dependent 

77names are defined in the ``language`` subpackage.""" 

78 

79_directives = {} 

80"""Cache of imported directives.""" 

81 

82 

83def directive(directive_name, language_module, document): 

84 """ 

85 Locate and return a directive function from its language-dependent name. 

86 If not found in the current language, check English. Return None if the 

87 named directive cannot be found. 

88 """ 

89 normname = directive_name.lower() 

90 messages = [] 

91 msg_text = [] 

92 if normname in _directives: 

93 return _directives[normname], messages 

94 canonicalname = None 

95 try: 

96 canonicalname = language_module.directives[normname] 

97 except AttributeError as error: 

98 msg_text.append('Problem retrieving directive entry from language ' 

99 'module %r: %s.' % (language_module, error)) 

100 except KeyError: 

101 msg_text.append('No directive entry for "%s" in module "%s".' 

102 % (directive_name, language_module.__name__)) 

103 if not canonicalname: 

104 try: 

105 canonicalname = _fallback_language_module.directives[normname] 

106 msg_text.append('Using English fallback for directive "%s".' 

107 % directive_name) 

108 except KeyError: 

109 msg_text.append('Trying "%s" as canonical directive name.' 

110 % directive_name) 

111 # The canonical name should be an English name, but just in case: 

112 canonicalname = normname 

113 if msg_text: 

114 message = document.reporter.info( 

115 '\n'.join(msg_text), line=document.current_line) 

116 messages.append(message) 

117 try: 

118 modulename, classname = _directive_registry[canonicalname] 

119 except KeyError: 

120 # Error handling done by caller. 

121 return None, messages 

122 try: 

123 module = import_module('docutils.parsers.rst.directives.'+modulename) 

124 except ImportError as detail: 

125 messages.append(document.reporter.error( 

126 'Error importing directive module "%s" (directive "%s"):\n%s' 

127 % (modulename, directive_name, detail), 

128 line=document.current_line)) 

129 return None, messages 

130 try: 

131 directive = getattr(module, classname) 

132 _directives[normname] = directive 

133 except AttributeError: 

134 messages.append(document.reporter.error( 

135 'No directive class "%s" in module "%s" (directive "%s").' 

136 % (classname, modulename, directive_name), 

137 line=document.current_line)) 

138 return None, messages 

139 return directive, messages 

140 

141 

142def register_directive(name, directive) -> None: 

143 """ 

144 Register a nonstandard application-defined directive function. 

145 Language lookups are not needed for such functions. 

146 """ 

147 _directives[name] = directive 

148 

149 

150# conversion functions for `Directive.option_spec` 

151# ------------------------------------------------ 

152# 

153# see also `parsers.rst.Directive` in ../__init__.py. 

154 

155 

156def flag(argument: str) -> None: 

157 """ 

158 Check for a valid flag option (no argument) and return ``None``. 

159 (Directive option conversion function.) 

160 

161 Raise ``ValueError`` if an argument is found. 

162 """ 

163 if argument and argument.strip(): 

164 raise ValueError('no argument is allowed; "%s" supplied' % argument) 

165 else: 

166 return None 

167 

168 

169def unchanged_required(argument: str) -> str: 

170 """ 

171 Return the argument text, unchanged. 

172 

173 Directive option conversion function for options that require a value. 

174 

175 Raise ``ValueError`` if no argument is found. 

176 """ 

177 if argument is None: 

178 raise ValueError('argument required but none supplied') 

179 else: 

180 return argument # unchanged! 

181 

182 

183def unchanged(argument: str) -> str: 

184 """ 

185 Return the argument text, unchanged. 

186 (Directive option conversion function.) 

187 

188 No argument implies empty string (""). 

189 """ 

190 if argument is None: 

191 return '' 

192 else: 

193 return argument # unchanged! 

194 

195 

196def path(argument: str) -> str: 

197 """ 

198 Return the path argument unwrapped (with newlines removed). 

199 (Directive option conversion function.) 

200 

201 Raise ``ValueError`` if no argument is found. 

202 """ 

203 if argument is None: 

204 raise ValueError('argument required but none supplied') 

205 else: 

206 return ''.join(s.strip() for s in argument.splitlines()) 

207 

208 

209def uri(argument: str) -> str: 

210 """ 

211 Return the URI argument with unescaped whitespace removed. 

212 (Directive option conversion function.) 

213 

214 Raise ``ValueError`` if no argument is found. 

215 """ 

216 if argument is None: 

217 raise ValueError('argument required but none supplied') 

218 else: 

219 parts = split_escaped_whitespace(escape2null(argument)) 

220 return ' '.join(''.join(nodes.unescape(part).split()) 

221 for part in parts) 

222 

223 

224def nonnegative_int(argument: str) -> int: 

225 """ 

226 Check for a nonnegative integer argument; raise ``ValueError`` if not. 

227 (Directive option conversion function.) 

228 """ 

229 value = int(argument) 

230 if value < 0: 

231 raise ValueError('negative value; must be positive or zero') 

232 return value 

233 

234 

235def percentage(argument: str) -> int: 

236 """ 

237 Check for an integer percentage value with optional percent sign. 

238 (Directive option conversion function.) 

239 """ 

240 try: 

241 argument = argument.rstrip(' %') 

242 except AttributeError: 

243 pass 

244 return nonnegative_int(argument) 

245 

246 

247CSS3_LENGTH_UNITS = ('em', 'ex', 'ch', 'rem', 'vw', 'vh', 'vmin', 'vmax', 

248 'cm', 'mm', 'Q', 'in', 'pt', 'pc', 'px') 

249"""Length units that are supported by the reStructuredText parser. 

250 

251Corresponds to the `length units in CSS3`__. 

252 

253__ https://www.w3.org/TR/css-values-3/#lengths 

254""" 

255 

256length_units = [*CSS3_LENGTH_UNITS] 

257"""Deprecated, will be removed in Docutils 0.24 or equivalent.""" 

258 

259 

260def get_measure(argument, units): 

261 """ 

262 Check for a positive argument of one of the `units`. 

263 

264 Return a normalized string of the form "<value><unit>" 

265 (without space inbetween). 

266 

267 To be called from directive option conversion functions. 

268 """ 

269 value, unit = nodes.parse_measure(argument) 

270 if value < 0 or unit not in units: 

271 raise ValueError( 

272 'not a positive number or measure of one of the following units:\n' 

273 + ', '.join(u for u in units if u)) 

274 return f'{value}{unit}' 

275 

276 

277def length_or_unitless(argument: str) -> str: 

278 return get_measure(argument, CSS3_LENGTH_UNITS + ('',)) 

279 

280 

281def length_or_percentage_or_unitless(argument, default=''): 

282 """ 

283 Return normalized string of a length or percentage unit. 

284 (Directive option conversion function.) 

285 

286 Add <default> if there is no unit. Raise ValueError if the argument is not 

287 a positive measure of one of the valid CSS units (or without unit). 

288 

289 >>> length_or_percentage_or_unitless('3 pt') 

290 '3pt' 

291 >>> length_or_percentage_or_unitless('3%', 'em') 

292 '3%' 

293 >>> length_or_percentage_or_unitless('3') 

294 '3' 

295 >>> length_or_percentage_or_unitless('3', 'px') 

296 '3px' 

297 """ 

298 try: 

299 return get_measure(argument, CSS3_LENGTH_UNITS + ('%',)) 

300 except ValueError as error: 

301 try: 

302 return get_measure(argument, ['']) + default 

303 except ValueError: 

304 raise error 

305 

306 

307def class_option(argument: str) -> list[str]: 

308 """ 

309 Convert the argument into a list of ID-compatible strings and return it. 

310 (Directive option conversion function.) 

311 

312 Raise ``ValueError`` if no argument is found. 

313 """ 

314 if argument is None: 

315 raise ValueError('argument required but none supplied') 

316 names = argument.split() 

317 class_names = [] 

318 for name in names: 

319 class_name = nodes.make_id(name) 

320 if not class_name: 

321 raise ValueError('cannot make "%s" into a class name' % name) 

322 class_names.append(class_name) 

323 return class_names 

324 

325 

326unicode_pattern = re.compile( 

327 r'(?:0x|x|\\x|U\+?|\\u)([0-9a-f]+)$|&#x([0-9a-f]+);$', re.IGNORECASE) 

328 

329 

330def unicode_code(code): 

331 r""" 

332 Convert a Unicode character code to a Unicode character. 

333 (Directive option conversion function.) 

334 

335 Codes may be decimal numbers, hexadecimal numbers (prefixed by ``0x``, 

336 ``x``, ``\x``, ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style 

337 numeric character entities (e.g. ``&#x262E;``). Other text remains as-is. 

338 

339 Raise ValueError for illegal Unicode code values. 

340 """ 

341 try: 

342 if code.isdigit(): # decimal number 

343 return chr(int(code)) 

344 else: 

345 match = unicode_pattern.match(code) 

346 if match: # hex number 

347 value = match.group(1) or match.group(2) 

348 return chr(int(value, 16)) 

349 else: # other text 

350 return code 

351 except OverflowError as detail: 

352 raise ValueError('code too large (%s)' % detail) 

353 

354 

355def single_char_or_unicode(argument: str) -> str: 

356 """ 

357 A single character is returned as-is. Unicode character codes are 

358 converted as in `unicode_code`. (Directive option conversion function.) 

359 """ 

360 char = unicode_code(argument) 

361 if len(char) > 1: 

362 raise ValueError('%r invalid; must be a single character or ' 

363 'a Unicode code' % char) 

364 return char 

365 

366 

367def single_char_or_whitespace_or_unicode(argument: str) -> str: 

368 """ 

369 As with `single_char_or_unicode`, but "tab" and "space" are also supported. 

370 (Directive option conversion function.) 

371 """ 

372 if argument == 'tab': 

373 char = '\t' 

374 elif argument == 'space': 

375 char = ' ' 

376 else: 

377 char = single_char_or_unicode(argument) 

378 return char 

379 

380 

381def positive_int(argument: str) -> int: 

382 """ 

383 Converts the argument into an integer. Raises ValueError for negative, 

384 zero, or non-integer values. (Directive option conversion function.) 

385 """ 

386 value = int(argument) 

387 if value < 1: 

388 raise ValueError('negative or zero value; must be positive') 

389 return value 

390 

391 

392def positive_int_list(argument: str) -> list[int]: 

393 """ 

394 Converts a space- or comma-separated list of values into a Python list 

395 of integers. 

396 (Directive option conversion function.) 

397 

398 Raises ValueError for non-positive-integer values. 

399 """ 

400 if ',' in argument: 

401 entries = argument.split(',') 

402 else: 

403 entries = argument.split() 

404 return [positive_int(entry) for entry in entries] 

405 

406 

407def encoding(argument: str) -> str: 

408 """ 

409 Verifies the encoding argument by lookup. 

410 (Directive option conversion function.) 

411 

412 Raises ValueError for unknown encodings. 

413 """ 

414 try: 

415 codecs.lookup(argument) 

416 except LookupError: 

417 raise ValueError('unknown encoding: "%s"' % argument) 

418 return argument 

419 

420 

421def choice(argument, values): 

422 """ 

423 Directive option utility function, supplied to enable options whose 

424 argument must be a member of a finite set of possible values (must be 

425 lower case). A custom conversion function must be written to use it. For 

426 example:: 

427 

428 from docutils.parsers.rst import directives 

429 

430 def yesno(argument: str): 

431 return directives.choice(argument, ('yes', 'no')) 

432 

433 Raise ``ValueError`` if no argument is found or if the argument's value is 

434 not valid (not an entry in the supplied list). 

435 """ 

436 try: 

437 value = argument.lower().strip() 

438 except AttributeError: 

439 raise ValueError('must supply an argument; choose from %s' 

440 % format_values(values)) 

441 if value in values: 

442 return value 

443 else: 

444 raise ValueError('"%s" unknown; choose from %s' 

445 % (argument, format_values(values))) 

446 

447 

448def format_values(values) -> str: 

449 return '%s, or "%s"' % (', '.join('"%s"' % s for s in values[:-1]), 

450 values[-1]) 

451 

452 

453def value_or(values: Sequence[str], other: type) -> Callable: 

454 """ 

455 Directive option conversion function. 

456 

457 The argument can be any of `values` or `argument_type`. 

458 """ 

459 def auto_or_other(argument: str): 

460 if argument in values: 

461 return argument 

462 else: 

463 return other(argument) 

464 return auto_or_other 

465 

466 

467def parser_name(argument: str) -> type[parsers.Parser]: 

468 """ 

469 Return a docutils parser whose name matches the argument. 

470 (Directive option conversion function.) 

471 

472 Return `None`, if the argument evaluates to `False`. 

473 Raise `ValueError` if importing the parser module fails. 

474 """ 

475 if not argument: 

476 return None 

477 try: 

478 return parsers.get_parser_class(argument) 

479 except ImportError as err: 

480 raise ValueError(str(err))