Mercurial > repos > guerler > springsuite
comparison planemo/lib/python3.7/site-packages/docutils/parsers/rst/directives/misc.py @ 0:d30785e31577 draft
"planemo upload commit 6eee67778febed82ddd413c3ca40b3183a3898f1"
author | guerler |
---|---|
date | Fri, 31 Jul 2020 00:18:57 -0400 |
parents | |
children |
comparison
equal
deleted
inserted
replaced
-1:000000000000 | 0:d30785e31577 |
---|---|
1 # $Id: misc.py 8370 2019-08-27 12:10:39Z milde $ | |
2 # Authors: David Goodger <goodger@python.org>; Dethe Elza | |
3 # Copyright: This module has been placed in the public domain. | |
4 | |
5 """Miscellaneous directives.""" | |
6 | |
7 __docformat__ = 'reStructuredText' | |
8 | |
9 import sys | |
10 import os.path | |
11 import re | |
12 import time | |
13 from docutils import io, nodes, statemachine, utils | |
14 from docutils.utils.error_reporting import SafeString, ErrorString | |
15 from docutils.utils.error_reporting import locale_encoding | |
16 from docutils.parsers.rst import Directive, convert_directive_function | |
17 from docutils.parsers.rst import directives, roles, states | |
18 from docutils.parsers.rst.directives.body import CodeBlock, NumberLines | |
19 from docutils.parsers.rst.roles import set_classes | |
20 from docutils.transforms import misc | |
21 | |
22 class Include(Directive): | |
23 | |
24 """ | |
25 Include content read from a separate source file. | |
26 | |
27 Content may be parsed by the parser, or included as a literal | |
28 block. The encoding of the included file can be specified. Only | |
29 a part of the given file argument may be included by specifying | |
30 start and end line or text to match before and/or after the text | |
31 to be used. | |
32 """ | |
33 | |
34 required_arguments = 1 | |
35 optional_arguments = 0 | |
36 final_argument_whitespace = True | |
37 option_spec = {'literal': directives.flag, | |
38 'code': directives.unchanged, | |
39 'encoding': directives.encoding, | |
40 'tab-width': int, | |
41 'start-line': int, | |
42 'end-line': int, | |
43 'start-after': directives.unchanged_required, | |
44 'end-before': directives.unchanged_required, | |
45 # ignored except for 'literal' or 'code': | |
46 'number-lines': directives.unchanged, # integer or None | |
47 'class': directives.class_option, | |
48 'name': directives.unchanged} | |
49 | |
50 standard_include_path = os.path.join(os.path.dirname(states.__file__), | |
51 'include') | |
52 | |
53 def run(self): | |
54 """Include a file as part of the content of this reST file.""" | |
55 if not self.state.document.settings.file_insertion_enabled: | |
56 raise self.warning('"%s" directive disabled.' % self.name) | |
57 source = self.state_machine.input_lines.source( | |
58 self.lineno - self.state_machine.input_offset - 1) | |
59 source_dir = os.path.dirname(os.path.abspath(source)) | |
60 path = directives.path(self.arguments[0]) | |
61 if path.startswith('<') and path.endswith('>'): | |
62 path = os.path.join(self.standard_include_path, path[1:-1]) | |
63 path = os.path.normpath(os.path.join(source_dir, path)) | |
64 path = utils.relative_path(None, path) | |
65 path = nodes.reprunicode(path) | |
66 encoding = self.options.get( | |
67 'encoding', self.state.document.settings.input_encoding) | |
68 e_handler=self.state.document.settings.input_encoding_error_handler | |
69 tab_width = self.options.get( | |
70 'tab-width', self.state.document.settings.tab_width) | |
71 try: | |
72 self.state.document.settings.record_dependencies.add(path) | |
73 include_file = io.FileInput(source_path=path, | |
74 encoding=encoding, | |
75 error_handler=e_handler) | |
76 except UnicodeEncodeError as error: | |
77 raise self.severe(u'Problems with "%s" directive path:\n' | |
78 'Cannot encode input file path "%s" ' | |
79 '(wrong locale?).' % | |
80 (self.name, SafeString(path))) | |
81 except IOError as error: | |
82 raise self.severe(u'Problems with "%s" directive path:\n%s.' % | |
83 (self.name, ErrorString(error))) | |
84 startline = self.options.get('start-line', None) | |
85 endline = self.options.get('end-line', None) | |
86 try: | |
87 if startline or (endline is not None): | |
88 lines = include_file.readlines() | |
89 rawtext = ''.join(lines[startline:endline]) | |
90 else: | |
91 rawtext = include_file.read() | |
92 except UnicodeError as error: | |
93 raise self.severe(u'Problem with "%s" directive:\n%s' % | |
94 (self.name, ErrorString(error))) | |
95 # start-after/end-before: no restrictions on newlines in match-text, | |
96 # and no restrictions on matching inside lines vs. line boundaries | |
97 after_text = self.options.get('start-after', None) | |
98 if after_text: | |
99 # skip content in rawtext before *and incl.* a matching text | |
100 after_index = rawtext.find(after_text) | |
101 if after_index < 0: | |
102 raise self.severe('Problem with "start-after" option of "%s" ' | |
103 'directive:\nText not found.' % self.name) | |
104 rawtext = rawtext[after_index + len(after_text):] | |
105 before_text = self.options.get('end-before', None) | |
106 if before_text: | |
107 # skip content in rawtext after *and incl.* a matching text | |
108 before_index = rawtext.find(before_text) | |
109 if before_index < 0: | |
110 raise self.severe('Problem with "end-before" option of "%s" ' | |
111 'directive:\nText not found.' % self.name) | |
112 rawtext = rawtext[:before_index] | |
113 | |
114 include_lines = statemachine.string2lines(rawtext, tab_width, | |
115 convert_whitespace=True) | |
116 if 'literal' in self.options: | |
117 # Don't convert tabs to spaces, if `tab_width` is positive. | |
118 if tab_width >= 0: | |
119 text = rawtext.expandtabs(tab_width) | |
120 else: | |
121 text = rawtext | |
122 literal_block = nodes.literal_block(rawtext, source=path, | |
123 classes=self.options.get('class', [])) | |
124 literal_block.line = 1 | |
125 self.add_name(literal_block) | |
126 if 'number-lines' in self.options: | |
127 try: | |
128 startline = int(self.options['number-lines'] or 1) | |
129 except ValueError: | |
130 raise self.error(':number-lines: with non-integer ' | |
131 'start value') | |
132 endline = startline + len(include_lines) | |
133 if text.endswith('\n'): | |
134 text = text[:-1] | |
135 tokens = NumberLines([([], text)], startline, endline) | |
136 for classes, value in tokens: | |
137 if classes: | |
138 literal_block += nodes.inline(value, value, | |
139 classes=classes) | |
140 else: | |
141 literal_block += nodes.Text(value) | |
142 else: | |
143 literal_block += nodes.Text(text) | |
144 return [literal_block] | |
145 if 'code' in self.options: | |
146 self.options['source'] = path | |
147 # Don't convert tabs to spaces, if `tab_width` is negative: | |
148 if tab_width < 0: | |
149 include_lines = rawtext.splitlines() | |
150 codeblock = CodeBlock(self.name, | |
151 [self.options.pop('code')], # arguments | |
152 self.options, | |
153 include_lines, # content | |
154 self.lineno, | |
155 self.content_offset, | |
156 self.block_text, | |
157 self.state, | |
158 self.state_machine) | |
159 return codeblock.run() | |
160 self.state_machine.insert_input(include_lines, path) | |
161 return [] | |
162 | |
163 | |
164 class Raw(Directive): | |
165 | |
166 """ | |
167 Pass through content unchanged | |
168 | |
169 Content is included in output based on type argument | |
170 | |
171 Content may be included inline (content section of directive) or | |
172 imported from a file or url. | |
173 """ | |
174 | |
175 required_arguments = 1 | |
176 optional_arguments = 0 | |
177 final_argument_whitespace = True | |
178 option_spec = {'file': directives.path, | |
179 'url': directives.uri, | |
180 'encoding': directives.encoding} | |
181 has_content = True | |
182 | |
183 def run(self): | |
184 if (not self.state.document.settings.raw_enabled | |
185 or (not self.state.document.settings.file_insertion_enabled | |
186 and ('file' in self.options | |
187 or 'url' in self.options))): | |
188 raise self.warning('"%s" directive disabled.' % self.name) | |
189 attributes = {'format': ' '.join(self.arguments[0].lower().split())} | |
190 encoding = self.options.get( | |
191 'encoding', self.state.document.settings.input_encoding) | |
192 e_handler=self.state.document.settings.input_encoding_error_handler | |
193 if self.content: | |
194 if 'file' in self.options or 'url' in self.options: | |
195 raise self.error( | |
196 '"%s" directive may not both specify an external file ' | |
197 'and have content.' % self.name) | |
198 text = '\n'.join(self.content) | |
199 elif 'file' in self.options: | |
200 if 'url' in self.options: | |
201 raise self.error( | |
202 'The "file" and "url" options may not be simultaneously ' | |
203 'specified for the "%s" directive.' % self.name) | |
204 source_dir = os.path.dirname( | |
205 os.path.abspath(self.state.document.current_source)) | |
206 path = os.path.normpath(os.path.join(source_dir, | |
207 self.options['file'])) | |
208 path = utils.relative_path(None, path) | |
209 try: | |
210 raw_file = io.FileInput(source_path=path, | |
211 encoding=encoding, | |
212 error_handler=e_handler) | |
213 # TODO: currently, raw input files are recorded as | |
214 # dependencies even if not used for the chosen output format. | |
215 self.state.document.settings.record_dependencies.add(path) | |
216 except IOError as error: | |
217 raise self.severe(u'Problems with "%s" directive path:\n%s.' | |
218 % (self.name, ErrorString(error))) | |
219 try: | |
220 text = raw_file.read() | |
221 except UnicodeError as error: | |
222 raise self.severe(u'Problem with "%s" directive:\n%s' | |
223 % (self.name, ErrorString(error))) | |
224 attributes['source'] = path | |
225 elif 'url' in self.options: | |
226 source = self.options['url'] | |
227 # Do not import urllib2 at the top of the module because | |
228 # it may fail due to broken SSL dependencies, and it takes | |
229 # about 0.15 seconds to load. | |
230 if sys.version_info >= (3, 0): | |
231 from urllib.request import urlopen | |
232 from urllib.error import URLError | |
233 else: | |
234 from urllib2 import urlopen, URLError | |
235 try: | |
236 raw_text = urlopen(source).read() | |
237 except (URLError, IOError, OSError) as error: | |
238 raise self.severe(u'Problems with "%s" directive URL "%s":\n%s.' | |
239 % (self.name, self.options['url'], ErrorString(error))) | |
240 raw_file = io.StringInput(source=raw_text, source_path=source, | |
241 encoding=encoding, | |
242 error_handler=e_handler) | |
243 try: | |
244 text = raw_file.read() | |
245 except UnicodeError as error: | |
246 raise self.severe(u'Problem with "%s" directive:\n%s' | |
247 % (self.name, ErrorString(error))) | |
248 attributes['source'] = source | |
249 else: | |
250 # This will always fail because there is no content. | |
251 self.assert_has_content() | |
252 raw_node = nodes.raw('', text, **attributes) | |
253 (raw_node.source, | |
254 raw_node.line) = self.state_machine.get_source_and_line(self.lineno) | |
255 return [raw_node] | |
256 | |
257 | |
258 class Replace(Directive): | |
259 | |
260 has_content = True | |
261 | |
262 def run(self): | |
263 if not isinstance(self.state, states.SubstitutionDef): | |
264 raise self.error( | |
265 'Invalid context: the "%s" directive can only be used within ' | |
266 'a substitution definition.' % self.name) | |
267 self.assert_has_content() | |
268 text = '\n'.join(self.content) | |
269 element = nodes.Element(text) | |
270 self.state.nested_parse(self.content, self.content_offset, | |
271 element) | |
272 # element might contain [paragraph] + system_message(s) | |
273 node = None | |
274 messages = [] | |
275 for elem in element: | |
276 if not node and isinstance(elem, nodes.paragraph): | |
277 node = elem | |
278 elif isinstance(elem, nodes.system_message): | |
279 elem['backrefs'] = [] | |
280 messages.append(elem) | |
281 else: | |
282 return [ | |
283 self.state_machine.reporter.error( | |
284 'Error in "%s" directive: may contain a single paragraph ' | |
285 'only.' % (self.name), line=self.lineno) ] | |
286 if node: | |
287 return messages + node.children | |
288 return messages | |
289 | |
290 class Unicode(Directive): | |
291 | |
292 r""" | |
293 Convert Unicode character codes (numbers) to characters. Codes may be | |
294 decimal numbers, hexadecimal numbers (prefixed by ``0x``, ``x``, ``\x``, | |
295 ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style numeric character | |
296 entities (e.g. ``☮``). Text following ".." is a comment and is | |
297 ignored. Spaces are ignored, and any other text remains as-is. | |
298 """ | |
299 | |
300 required_arguments = 1 | |
301 optional_arguments = 0 | |
302 final_argument_whitespace = True | |
303 option_spec = {'trim': directives.flag, | |
304 'ltrim': directives.flag, | |
305 'rtrim': directives.flag} | |
306 | |
307 comment_pattern = re.compile(r'( |\n|^)\.\. ') | |
308 | |
309 def run(self): | |
310 if not isinstance(self.state, states.SubstitutionDef): | |
311 raise self.error( | |
312 'Invalid context: the "%s" directive can only be used within ' | |
313 'a substitution definition.' % self.name) | |
314 substitution_definition = self.state_machine.node | |
315 if 'trim' in self.options: | |
316 substitution_definition.attributes['ltrim'] = 1 | |
317 substitution_definition.attributes['rtrim'] = 1 | |
318 if 'ltrim' in self.options: | |
319 substitution_definition.attributes['ltrim'] = 1 | |
320 if 'rtrim' in self.options: | |
321 substitution_definition.attributes['rtrim'] = 1 | |
322 codes = self.comment_pattern.split(self.arguments[0])[0].split() | |
323 element = nodes.Element() | |
324 for code in codes: | |
325 try: | |
326 decoded = directives.unicode_code(code) | |
327 except ValueError as error: | |
328 raise self.error(u'Invalid character code: %s\n%s' | |
329 % (code, ErrorString(error))) | |
330 element += nodes.Text(decoded) | |
331 return element.children | |
332 | |
333 | |
334 class Class(Directive): | |
335 | |
336 """ | |
337 Set a "class" attribute on the directive content or the next element. | |
338 When applied to the next element, a "pending" element is inserted, and a | |
339 transform does the work later. | |
340 """ | |
341 | |
342 required_arguments = 1 | |
343 optional_arguments = 0 | |
344 final_argument_whitespace = True | |
345 has_content = True | |
346 | |
347 def run(self): | |
348 try: | |
349 class_value = directives.class_option(self.arguments[0]) | |
350 except ValueError: | |
351 raise self.error( | |
352 'Invalid class attribute value for "%s" directive: "%s".' | |
353 % (self.name, self.arguments[0])) | |
354 node_list = [] | |
355 if self.content: | |
356 container = nodes.Element() | |
357 self.state.nested_parse(self.content, self.content_offset, | |
358 container) | |
359 for node in container: | |
360 node['classes'].extend(class_value) | |
361 node_list.extend(container.children) | |
362 else: | |
363 pending = nodes.pending( | |
364 misc.ClassAttribute, | |
365 {'class': class_value, 'directive': self.name}, | |
366 self.block_text) | |
367 self.state_machine.document.note_pending(pending) | |
368 node_list.append(pending) | |
369 return node_list | |
370 | |
371 | |
372 class Role(Directive): | |
373 | |
374 has_content = True | |
375 | |
376 argument_pattern = re.compile(r'(%s)\s*(\(\s*(%s)\s*\)\s*)?$' | |
377 % ((states.Inliner.simplename,) * 2)) | |
378 | |
379 def run(self): | |
380 """Dynamically create and register a custom interpreted text role.""" | |
381 if self.content_offset > self.lineno or not self.content: | |
382 raise self.error('"%s" directive requires arguments on the first ' | |
383 'line.' % self.name) | |
384 args = self.content[0] | |
385 match = self.argument_pattern.match(args) | |
386 if not match: | |
387 raise self.error('"%s" directive arguments not valid role names: ' | |
388 '"%s".' % (self.name, args)) | |
389 new_role_name = match.group(1) | |
390 base_role_name = match.group(3) | |
391 messages = [] | |
392 if base_role_name: | |
393 base_role, messages = roles.role( | |
394 base_role_name, self.state_machine.language, self.lineno, | |
395 self.state.reporter) | |
396 if base_role is None: | |
397 error = self.state.reporter.error( | |
398 'Unknown interpreted text role "%s".' % base_role_name, | |
399 nodes.literal_block(self.block_text, self.block_text), | |
400 line=self.lineno) | |
401 return messages + [error] | |
402 else: | |
403 base_role = roles.generic_custom_role | |
404 assert not hasattr(base_role, 'arguments'), ( | |
405 'Supplemental directive arguments for "%s" directive not ' | |
406 'supported (specified by "%r" role).' % (self.name, base_role)) | |
407 try: | |
408 converted_role = convert_directive_function(base_role) | |
409 (arguments, options, content, content_offset) = ( | |
410 self.state.parse_directive_block( | |
411 self.content[1:], self.content_offset, converted_role, | |
412 option_presets={})) | |
413 except states.MarkupError as detail: | |
414 error = self.state_machine.reporter.error( | |
415 'Error in "%s" directive:\n%s.' % (self.name, detail), | |
416 nodes.literal_block(self.block_text, self.block_text), | |
417 line=self.lineno) | |
418 return messages + [error] | |
419 if 'class' not in options: | |
420 try: | |
421 options['class'] = directives.class_option(new_role_name) | |
422 except ValueError as detail: | |
423 error = self.state_machine.reporter.error( | |
424 u'Invalid argument for "%s" directive:\n%s.' | |
425 % (self.name, SafeString(detail)), nodes.literal_block( | |
426 self.block_text, self.block_text), line=self.lineno) | |
427 return messages + [error] | |
428 role = roles.CustomRole(new_role_name, base_role, options, content) | |
429 roles.register_local_role(new_role_name, role) | |
430 return messages | |
431 | |
432 | |
433 class DefaultRole(Directive): | |
434 | |
435 """Set the default interpreted text role.""" | |
436 | |
437 optional_arguments = 1 | |
438 final_argument_whitespace = False | |
439 | |
440 def run(self): | |
441 if not self.arguments: | |
442 if '' in roles._roles: | |
443 # restore the "default" default role | |
444 del roles._roles[''] | |
445 return [] | |
446 role_name = self.arguments[0] | |
447 role, messages = roles.role(role_name, self.state_machine.language, | |
448 self.lineno, self.state.reporter) | |
449 if role is None: | |
450 error = self.state.reporter.error( | |
451 'Unknown interpreted text role "%s".' % role_name, | |
452 nodes.literal_block(self.block_text, self.block_text), | |
453 line=self.lineno) | |
454 return messages + [error] | |
455 roles._roles[''] = role | |
456 return messages | |
457 | |
458 | |
459 class Title(Directive): | |
460 | |
461 required_arguments = 1 | |
462 optional_arguments = 0 | |
463 final_argument_whitespace = True | |
464 | |
465 def run(self): | |
466 self.state_machine.document['title'] = self.arguments[0] | |
467 return [] | |
468 | |
469 | |
470 class Date(Directive): | |
471 | |
472 has_content = True | |
473 | |
474 def run(self): | |
475 if not isinstance(self.state, states.SubstitutionDef): | |
476 raise self.error( | |
477 'Invalid context: the "%s" directive can only be used within ' | |
478 'a substitution definition.' % self.name) | |
479 format_str = '\n'.join(self.content) or '%Y-%m-%d' | |
480 if sys.version_info< (3, 0): | |
481 try: | |
482 format_str = format_str.encode(locale_encoding or 'utf-8') | |
483 except UnicodeEncodeError: | |
484 raise self.warning(u'Cannot encode date format string ' | |
485 u'with locale encoding "%s".' % locale_encoding) | |
486 # @@@ | |
487 # Use timestamp from the `SOURCE_DATE_EPOCH`_ environment variable? | |
488 # Pro: Docutils-generated documentation | |
489 # can easily be part of `reproducible software builds`__ | |
490 # | |
491 # __ https://reproducible-builds.org/ | |
492 # | |
493 # Con: Changes the specs, hard to predict behaviour, | |
494 # | |
495 # See also the discussion about \date \time \year in TeX | |
496 # http://tug.org/pipermail/tex-k/2016-May/002704.html | |
497 # source_date_epoch = os.environ.get('SOURCE_DATE_EPOCH') | |
498 # if (source_date_epoch): | |
499 # text = time.strftime(format_str, | |
500 # time.gmtime(int(source_date_epoch))) | |
501 # else: | |
502 text = time.strftime(format_str) | |
503 if sys.version_info< (3, 0): | |
504 # `text` is a byte string that may contain non-ASCII characters: | |
505 try: | |
506 text = text.decode(locale_encoding or 'utf-8') | |
507 except UnicodeDecodeError: | |
508 text = text.decode(locale_encoding or 'utf-8', 'replace') | |
509 raise self.warning(u'Error decoding "%s"' | |
510 u'with locale encoding "%s".' % (text, locale_encoding)) | |
511 return [nodes.Text(text)] | |
512 | |
513 | |
514 class TestDirective(Directive): | |
515 | |
516 """This directive is useful only for testing purposes.""" | |
517 | |
518 optional_arguments = 1 | |
519 final_argument_whitespace = True | |
520 option_spec = {'option': directives.unchanged_required} | |
521 has_content = True | |
522 | |
523 def run(self): | |
524 if self.content: | |
525 text = '\n'.join(self.content) | |
526 info = self.state_machine.reporter.info( | |
527 'Directive processed. Type="%s", arguments=%r, options=%r, ' | |
528 'content:' % (self.name, self.arguments, self.options), | |
529 nodes.literal_block(text, text), line=self.lineno) | |
530 else: | |
531 info = self.state_machine.reporter.info( | |
532 'Directive processed. Type="%s", arguments=%r, options=%r, ' | |
533 'content: None' % (self.name, self.arguments, self.options), | |
534 line=self.lineno) | |
535 return [info] | |
536 | |
537 # Old-style, functional definition: | |
538 # | |
539 # def directive_test_function(name, arguments, options, content, lineno, | |
540 # content_offset, block_text, state, state_machine): | |
541 # """This directive is useful only for testing purposes.""" | |
542 # if content: | |
543 # text = '\n'.join(content) | |
544 # info = state_machine.reporter.info( | |
545 # 'Directive processed. Type="%s", arguments=%r, options=%r, ' | |
546 # 'content:' % (name, arguments, options), | |
547 # nodes.literal_block(text, text), line=lineno) | |
548 # else: | |
549 # info = state_machine.reporter.info( | |
550 # 'Directive processed. Type="%s", arguments=%r, options=%r, ' | |
551 # 'content: None' % (name, arguments, options), line=lineno) | |
552 # return [info] | |
553 # | |
554 # directive_test_function.arguments = (0, 1, 1) | |
555 # directive_test_function.options = {'option': directives.unchanged_required} | |
556 # directive_test_function.content = 1 |