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