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

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

187 statements  

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. ``&#x262E;``). 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]