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

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

181 statements  

1# $Id: __init__.py 10343 2026-06-11 05:54:43Z 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, 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 

256 

257def get_measure(argument, units): 

258 """ 

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

260 

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

262 (without space inbetween). 

263 

264 To be called from directive option conversion functions. 

265 """ 

266 value, unit = nodes.parse_measure(argument) 

267 if value < 0 or unit not in units: 

268 raise ValueError( 

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

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

271 return f'{value}{unit}' 

272 

273 

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

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

276 

277 

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

279 """ 

280 Return normalized string of a length or percentage unit. 

281 (Directive option conversion function.) 

282 

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

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

285 

286 >>> length_or_percentage_or_unitless('3 pt') 

287 '3pt' 

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

289 '3%' 

290 >>> length_or_percentage_or_unitless('3') 

291 '3' 

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

293 '3px' 

294 """ 

295 try: 

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

297 except ValueError as error: 

298 try: 

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

300 except ValueError: 

301 raise error 

302 

303 

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

305 """ 

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

307 (Directive option conversion function.) 

308 

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

310 """ 

311 if argument is None: 

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

313 names = argument.split() 

314 class_names = [] 

315 for name in names: 

316 class_name = nodes.make_id(name) 

317 if not class_name: 

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

319 class_names.append(class_name) 

320 return class_names 

321 

322 

323unicode_pattern = re.compile( 

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

325 

326 

327def unicode_code(code): 

328 r""" 

329 Convert a Unicode character code to a Unicode character. 

330 (Directive option conversion function.) 

331 

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

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

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

335 

336 Raise ValueError for illegal Unicode code values. 

337 """ 

338 try: 

339 if code.isdigit(): # decimal number 

340 return chr(int(code)) 

341 else: 

342 match = unicode_pattern.match(code) 

343 if match: # hex number 

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

345 return chr(int(value, 16)) 

346 else: # other text 

347 return code 

348 except OverflowError as detail: 

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

350 

351 

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

353 """ 

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

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

356 """ 

357 char = unicode_code(argument) 

358 if len(char) > 1: 

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

360 'a Unicode code' % char) 

361 return char 

362 

363 

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

365 """ 

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

367 (Directive option conversion function.) 

368 """ 

369 if argument == 'tab': 

370 char = '\t' 

371 elif argument == 'space': 

372 char = ' ' 

373 else: 

374 char = single_char_or_unicode(argument) 

375 return char 

376 

377 

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

379 """ 

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

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

382 """ 

383 value = int(argument) 

384 if value < 1: 

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

386 return value 

387 

388 

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

390 """ 

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

392 of integers. 

393 (Directive option conversion function.) 

394 

395 Raises ValueError for non-positive-integer values. 

396 """ 

397 if ',' in argument: 

398 entries = argument.split(',') 

399 else: 

400 entries = argument.split() 

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

402 

403 

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

405 """ 

406 Verifies the encoding argument by lookup. 

407 (Directive option conversion function.) 

408 

409 Raises ValueError for unknown encodings. 

410 """ 

411 try: 

412 codecs.lookup(argument) 

413 except LookupError: 

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

415 return argument 

416 

417 

418def choice(argument, values): 

419 """ 

420 Directive option utility function, supplied to enable options whose 

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

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

423 example:: 

424 

425 from docutils.parsers.rst import directives 

426 

427 def yesno(argument: str): 

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

429 

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

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

432 """ 

433 try: 

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

435 except AttributeError: 

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

437 % format_values(values)) 

438 if value in values: 

439 return value 

440 else: 

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

442 % (argument, format_values(values))) 

443 

444 

445def format_values(values) -> str: 

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

447 values[-1]) 

448 

449 

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

451 """ 

452 Directive option conversion function. 

453 

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

455 """ 

456 def auto_or_other(argument: str): 

457 if argument in values: 

458 return argument 

459 else: 

460 return other(argument) 

461 return auto_or_other 

462 

463 

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

465 """ 

466 Return a docutils parser whose name matches the argument. 

467 (Directive option conversion function.) 

468 

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

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

471 """ 

472 if not argument: 

473 return None 

474 try: 

475 return parsers.get_parser_class(argument) 

476 except ImportError as err: 

477 raise ValueError(str(err))