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

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

207 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_name, rawtext, text, lineno, inliner, 

347 options=None, content=None): 

348 options = normalize_options(options) 

349 # syntax highlight language (for derived custom roles) 

350 language = role_name if role_name != 'code' else '' 

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

352 if language.lower() == 'none': 

353 language = '' # disable syntax highlight 

354 classes = ['code'] 

355 if 'classes' in options: 

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

357 if language and language not in classes: 

358 classes.append(language) 

359 try: 

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

361 inliner.document.settings.syntax_highlight) 

362 except LexerError as error: 

363 if 'language' in options: 

364 msg = inliner.reporter.warning(error) 

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

366 return [prb], [msg] 

367 else: 

368 tokens = [('', nodes.unescape(text, True))] 

369 

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

371 

372 # analyse content and add nodes for every token 

373 for classes, value in tokens: 

374 if classes: 

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

376 else: 

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

378 node += nodes.Text(value) 

379 

380 return [node], [] 

381 

382 

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

384 

385register_canonical_role('code', code_role) 

386 

387 

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

389 options=None, content=None): 

390 options = normalize_options(options) 

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

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

393 return [node], [] 

394 

395 

396register_canonical_role('math', math_role) 

397 

398 

399###################################################################### 

400# Register roles that are currently unimplemented. 

401###################################################################### 

402 

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

404 options=None, content=None): 

405 msg = inliner.reporter.error( 

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

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

408 return [prb], [msg] 

409 

410 

411register_canonical_role('index', unimplemented_role) 

412register_canonical_role('named-reference', unimplemented_role) 

413register_canonical_role('anonymous-reference', unimplemented_role) 

414register_canonical_role('uri-reference', unimplemented_role) 

415register_canonical_role('footnote-reference', unimplemented_role) 

416register_canonical_role('citation-reference', unimplemented_role) 

417register_canonical_role('substitution-reference', unimplemented_role) 

418register_canonical_role('target', unimplemented_role) 

419 

420# This should remain unimplemented, for testing purposes: 

421register_canonical_role('restructuredtext-unimplemented-role', 

422 unimplemented_role) 

423 

424 

425def set_classes(options) -> None: 

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

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

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

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

430 if options and 'class' in options: 

431 assert 'classes' not in options 

432 options['classes'] = options['class'] 

433 del options['class'] 

434 

435 

436def normalized_role_options(options): 

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

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

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

440 return normalize_options(options) 

441 

442 

443def normalize_options(options): 

444 """ 

445 Return normalized dictionary of role/directive options. 

446 

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

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

449 """ 

450 if options is None: 

451 return {} 

452 n_options = options.copy() 

453 if 'class' in n_options: 

454 assert 'classes' not in n_options 

455 n_options['classes'] = n_options['class'] 

456 del n_options['class'] 

457 return n_options