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

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

202 statements  

1# $Id$ 

2# Author: Edward Loper <edloper@gradient.cis.upenn.edu> 

3# Copyright: This module has been placed in the public domain. 

4 

5""" 

6This module defines standard interpreted text role functions, a registry for 

7interpreted text roles, and an API for adding to and retrieving from the 

8registry. See also `Creating reStructuredText Interpreted Text Roles`__. 

9 

10__ https://docutils.sourceforge.io/docs/ref/rst/roles.html 

11 

12 

13The interface for interpreted role functions is as follows:: 

14 

15 def role_fn(name, rawtext, text, lineno, inliner, 

16 options=None, content=None): 

17 code... 

18 

19 # Set function attributes for customization: 

20 role_fn.options = ... 

21 role_fn.content = ... 

22 

23Parameters: 

24 

25- ``name`` is the local name of the interpreted text role, the role name 

26 actually used in the document. 

27 

28- ``rawtext`` is a string containing the entire interpreted text construct. 

29 Return it as a ``problematic`` node linked to a system message if there is a 

30 problem. 

31 

32- ``text`` is the interpreted text content, with backslash escapes converted 

33 to nulls (``\x00``). 

34 

35- ``lineno`` is the line number where the text block containing the 

36 interpreted text begins. 

37 

38- ``inliner`` is the Inliner object that called the role function. 

39 It defines the following useful attributes: ``reporter``, 

40 ``problematic``, ``memo``, ``parent``, ``document``. 

41 

42- ``options``: A dictionary of directive options for customization, to be 

43 interpreted by the role function. Used for additional attributes for the 

44 generated elements and other functionality. 

45 

46- ``content``: A list of strings, the directive content for customization 

47 ("role" directive). To be interpreted by the role function. 

48 

49Function attributes for customization, interpreted by the "role" directive: 

50 

51- ``options``: A dictionary, mapping known option names to conversion 

52 functions such as `int` or `float`. ``None`` or an empty dict implies no 

53 options to parse. Several directive option conversion functions are defined 

54 in the `directives` module. 

55 

56 All role functions implicitly support the "class" option, unless disabled 

57 with an explicit ``{'class': None}``. 

58 

59- ``content``: A boolean; true if content is allowed. Client code must handle 

60 the case where content is required but not supplied (an empty content list 

61 will be supplied). 

62 

63Note that unlike directives, the "arguments" function attribute is not 

64supported for role customization. Directive arguments are handled by the 

65"role" directive itself. 

66 

67Interpreted role functions return a tuple of two values: 

68 

69- A list of nodes which will be inserted into the document tree at the 

70 point where the interpreted role was encountered (can be an empty 

71 list). 

72 

73- A list of system messages, which will be inserted into the document tree 

74 immediately after the end of the current inline block (can also be empty). 

75""" 

76 

77from __future__ import annotations 

78 

79__docformat__ = 'reStructuredText' 

80 

81import warnings 

82 

83from docutils import nodes 

84from docutils.parsers.rst import directives 

85from docutils.parsers.rst.languages import en as _fallback_language_module 

86from docutils.utils.code_analyzer import Lexer, LexerError 

87 

88DEFAULT_INTERPRETED_ROLE = 'title-reference' 

89"""The canonical name of the default interpreted role. 

90 

91This role is used when no role is specified for a piece of interpreted text. 

92""" 

93 

94_role_registry = {} 

95"""Mapping of canonical role names to role functions. 

96 

97Language-dependent role names are defined in the ``language`` subpackage. 

98""" 

99 

100_roles = {} 

101"""Mapping of local or language-dependent interpreted text role names to role 

102functions.""" 

103 

104 

105def role(role_name, language_module, lineno, reporter): 

106 """ 

107 Locate and return a role function from its language-dependent name, along 

108 with a list of system messages. 

109 

110 If the role is not found in the current language, check English. Return a 

111 2-tuple: role function (``None`` if the named role cannot be found) and a 

112 list of system messages. 

113 """ 

114 normname = role_name.lower() 

115 messages = [] 

116 msg_text = [] 

117 

118 if normname in _roles: 

119 return _roles[normname], messages 

120 

121 if role_name: 

122 canonicalname = None 

123 try: 

124 canonicalname = language_module.roles[normname] 

125 except AttributeError as error: 

126 msg_text.append('Problem retrieving role entry from language ' 

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

128 except KeyError: 

129 msg_text.append('No role entry for "%s" in module "%s".' 

130 % (role_name, language_module.__name__)) 

131 else: 

132 canonicalname = DEFAULT_INTERPRETED_ROLE 

133 

134 # If we didn't find it, try English as a fallback. 

135 if not canonicalname: 

136 try: 

137 canonicalname = _fallback_language_module.roles[normname] 

138 msg_text.append('Using English fallback for role "%s".' 

139 % role_name) 

140 except KeyError: 

141 msg_text.append('Trying "%s" as canonical role name.' 

142 % role_name) 

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

144 canonicalname = normname 

145 

146 # Collect any messages that we generated. 

147 if msg_text: 

148 message = reporter.info('\n'.join(msg_text), line=lineno) 

149 messages.append(message) 

150 

151 # Look the role up in the registry, and return it. 

152 if canonicalname in _role_registry: 

153 role_fn = _role_registry[canonicalname] 

154 register_local_role(normname, role_fn) 

155 return role_fn, messages 

156 return None, messages # Error message will be generated by caller. 

157 

158 

159def register_canonical_role(name, role_fn) -> None: 

160 """ 

161 Register an interpreted text role by its canonical name. 

162 

163 :Parameters: 

164 - `name`: The canonical name of the interpreted role. 

165 - `role_fn`: The role function. See the module docstring. 

166 """ 

167 set_implicit_options(role_fn) 

168 _role_registry[name.lower()] = role_fn 

169 

170 

171def register_local_role(name, role_fn) -> None: 

172 """ 

173 Register an interpreted text role by its local or language-dependent name. 

174 

175 :Parameters: 

176 - `name`: The local or language-dependent name of the interpreted role. 

177 - `role_fn`: The role function. See the module docstring. 

178 """ 

179 set_implicit_options(role_fn) 

180 _roles[name.lower()] = role_fn 

181 

182 

183def set_implicit_options(role_fn) -> None: 

184 """ 

185 Add customization options to role functions, unless explicitly set or 

186 disabled. 

187 """ 

188 if not hasattr(role_fn, 'options') or role_fn.options is None: 

189 role_fn.options = {'class': directives.class_option} 

190 elif 'class' not in role_fn.options: 

191 role_fn.options['class'] = directives.class_option 

192 

193 

194def register_generic_role(canonical_name, node_class) -> None: 

195 """For roles which simply wrap a given `node_class` around the text.""" 

196 role = GenericRole(canonical_name, node_class) 

197 register_canonical_role(canonical_name, role) 

198 

199 

200class GenericRole: 

201 """ 

202 Generic interpreted text role. 

203 

204 The interpreted text is simply wrapped with the provided node class. 

205 """ 

206 

207 def __init__(self, role_name, node_class) -> None: 

208 self.name = role_name 

209 self.node_class = node_class 

210 

211 def __call__(self, role, rawtext, text, lineno, inliner, 

212 options=None, content=None): 

213 options = normalize_options(options) 

214 return [self.node_class(rawtext, text, **options)], [] 

215 

216 

217class CustomRole: 

218 """Wrapper for custom interpreted text roles.""" 

219 

220 def __init__( 

221 self, role_name, base_role, options=None, content=None, 

222 ) -> None: 

223 self.name = role_name 

224 self.base_role = base_role 

225 self.options = getattr(base_role, 'options', None) 

226 self.content = getattr(base_role, 'content', None) 

227 self.supplied_options = options 

228 self.supplied_content = content 

229 

230 def __call__(self, role, rawtext, text, lineno, inliner, 

231 options=None, content=None): 

232 opts = normalize_options(self.supplied_options) 

233 try: 

234 opts.update(options) 

235 except TypeError: # options may be ``None`` 

236 pass 

237 # pass concatenation of content from instance and call argument: 

238 supplied_content = self.supplied_content or [] 

239 content = content or [] 

240 delimiter = ['\n'] if supplied_content and content else [] 

241 return self.base_role(role, rawtext, text, lineno, inliner, 

242 options=opts, 

243 content=supplied_content+delimiter+content) 

244 

245 

246def generic_custom_role(role, rawtext, text, lineno, inliner, 

247 options=None, content=None): 

248 """Base for custom roles if no other base role is specified.""" 

249 # Once nested inline markup is implemented, this and other methods should 

250 # recursively call inliner.nested_parse(). 

251 options = normalize_options(options) 

252 return [nodes.inline(rawtext, text, **options)], [] 

253 

254 

255generic_custom_role.options = {'class': directives.class_option} 

256 

257 

258###################################################################### 

259# Define and register the standard roles: 

260###################################################################### 

261 

262register_generic_role('abbreviation', nodes.abbreviation) 

263register_generic_role('acronym', nodes.acronym) 

264register_generic_role('emphasis', nodes.emphasis) 

265register_generic_role('literal', nodes.literal) 

266register_generic_role('strong', nodes.strong) 

267register_generic_role('subscript', nodes.subscript) 

268register_generic_role('superscript', nodes.superscript) 

269register_generic_role('title-reference', nodes.title_reference) 

270 

271 

272def pep_reference_role(role, rawtext, text, lineno, inliner, 

273 options=None, content=None): 

274 options = normalize_options(options) 

275 try: 

276 pepnum = int(nodes.unescape(text)) 

277 if pepnum < 0 or pepnum > 9999: 

278 raise ValueError 

279 except ValueError: 

280 msg = inliner.reporter.error( 

281 'PEP number must be a number from 0 to 9999; "%s" is invalid.' 

282 % text, line=lineno) 

283 prb = inliner.problematic(rawtext, rawtext, msg) 

284 return [prb], [msg] 

285 # Base URL mainly used by inliner.pep_reference; so this is correct: 

286 ref = (inliner.document.settings.pep_base_url 

287 + inliner.document.settings.pep_file_url_template % pepnum) 

288 return [nodes.reference(rawtext, 'PEP ' + text, refuri=ref, **options)], [] 

289 

290 

291register_canonical_role('pep-reference', pep_reference_role) 

292 

293 

294def rfc_reference_role(role, rawtext, text, lineno, inliner, 

295 options=None, content=None): 

296 options = normalize_options(options) 

297 if "#" in text: 

298 rfcnum, section = nodes.unescape(text).split("#", 1) 

299 else: 

300 rfcnum, section = nodes.unescape(text), None 

301 try: 

302 rfcnum = int(rfcnum) 

303 if rfcnum < 1: 

304 raise ValueError 

305 except ValueError: 

306 msg = inliner.reporter.error( 

307 'RFC number must be a number greater than or equal to 1; ' 

308 '"%s" is invalid.' % text, line=lineno) 

309 prb = inliner.problematic(rawtext, rawtext, msg) 

310 return [prb], [msg] 

311 # Base URL mainly used by inliner.rfc_reference, so this is correct: 

312 ref = inliner.document.settings.rfc_base_url + inliner.rfc_url % rfcnum 

313 if section is not None: 

314 ref += "#" + section 

315 node = nodes.reference(rawtext, 'RFC '+str(rfcnum), refuri=ref, **options) 

316 return [node], [] 

317 

318 

319register_canonical_role('rfc-reference', rfc_reference_role) 

320 

321 

322def raw_role(role, rawtext, text, lineno, inliner, options=None, content=None): 

323 options = normalize_options(options) 

324 if not inliner.document.settings.raw_enabled: 

325 msg = inliner.reporter.warning('raw (and derived) roles disabled') 

326 prb = inliner.problematic(rawtext, rawtext, msg) 

327 return [prb], [msg] 

328 if 'format' not in options: 

329 msg = inliner.reporter.error( 

330 'No format (Writer name) is associated with this role: "%s".\n' 

331 'The "raw" role cannot be used directly.\n' 

332 'Instead, use the "role" directive to create a new role with ' 

333 'an associated format.' % role, line=lineno) 

334 prb = inliner.problematic(rawtext, rawtext, msg) 

335 return [prb], [msg] 

336 node = nodes.raw(rawtext, nodes.unescape(text, True), **options) 

337 node.source, node.line = inliner.reporter.get_source_and_line(lineno) 

338 return [node], [] 

339 

340 

341raw_role.options = {'format': directives.unchanged} 

342 

343register_canonical_role('raw', raw_role) 

344 

345 

346def code_role(role, rawtext, text, lineno, inliner, 

347 options=None, content=None): 

348 options = normalize_options(options) 

349 language = options.get('language', '') 

350 classes = ['code'] 

351 if 'classes' in options: 

352 classes.extend(options['classes']) 

353 if language and language not in classes: 

354 classes.append(language) 

355 try: 

356 tokens = Lexer(nodes.unescape(text, True), language, 

357 inliner.document.settings.syntax_highlight) 

358 except LexerError as error: 

359 msg = inliner.reporter.warning(error) 

360 prb = inliner.problematic(rawtext, rawtext, msg) 

361 return [prb], [msg] 

362 

363 node = nodes.literal(rawtext, '', classes=classes) 

364 

365 # analyse content and add nodes for every token 

366 for classes, value in tokens: 

367 if classes: 

368 node += nodes.inline(value, value, classes=classes) 

369 else: 

370 # insert as Text to decrease the verbosity of the output 

371 node += nodes.Text(value) 

372 

373 return [node], [] 

374 

375 

376code_role.options = {'language': directives.unchanged} 

377 

378register_canonical_role('code', code_role) 

379 

380 

381def math_role(role, rawtext, text, lineno, inliner, 

382 options=None, content=None): 

383 options = normalize_options(options) 

384 text = nodes.unescape(text, True) # raw text without inline role markup 

385 node = nodes.math(rawtext, text, **options) 

386 return [node], [] 

387 

388 

389register_canonical_role('math', math_role) 

390 

391 

392###################################################################### 

393# Register roles that are currently unimplemented. 

394###################################################################### 

395 

396def unimplemented_role(role, rawtext, text, lineno, inliner, 

397 options=None, content=None): 

398 msg = inliner.reporter.error( 

399 'Interpreted text role "%s" not implemented.' % role, line=lineno) 

400 prb = inliner.problematic(rawtext, rawtext, msg) 

401 return [prb], [msg] 

402 

403 

404register_canonical_role('index', unimplemented_role) 

405register_canonical_role('named-reference', unimplemented_role) 

406register_canonical_role('anonymous-reference', unimplemented_role) 

407register_canonical_role('uri-reference', unimplemented_role) 

408register_canonical_role('footnote-reference', unimplemented_role) 

409register_canonical_role('citation-reference', unimplemented_role) 

410register_canonical_role('substitution-reference', unimplemented_role) 

411register_canonical_role('target', unimplemented_role) 

412 

413# This should remain unimplemented, for testing purposes: 

414register_canonical_role('restructuredtext-unimplemented-role', 

415 unimplemented_role) 

416 

417 

418def set_classes(options) -> None: 

419 """Deprecated. Obsoleted by ``normalize_options()``.""" 

420 warnings.warn('The auxiliary function roles.set_classes() is obsoleted' 

421 ' by roles.normalize_options() and will be removed' 

422 ' in Docutils 2.0', PendingDeprecationWarning, stacklevel=2) 

423 if options and 'class' in options: 

424 assert 'classes' not in options 

425 options['classes'] = options['class'] 

426 del options['class'] 

427 

428 

429def normalized_role_options(options): 

430 warnings.warn('The auxiliary function roles.normalized_role_options() is ' 

431 'obsoleted by roles.normalize_options() and will be removed' 

432 ' in Docutils 2.0', PendingDeprecationWarning, stacklevel=2) 

433 return normalize_options(options) 

434 

435 

436def normalize_options(options): 

437 """ 

438 Return normalized dictionary of role/directive options. 

439 

440 * ``None`` is replaced by an empty dictionary. 

441 * The key 'class' is renamed to 'classes'. 

442 """ 

443 if options is None: 

444 return {} 

445 n_options = options.copy() 

446 if 'class' in n_options: 

447 assert 'classes' not in n_options 

448 n_options['classes'] = n_options['class'] 

449 del n_options['class'] 

450 return n_options