math, error_reporting, and urischemes moved to the utils package.
[docutils.git] / docutils / parsers / rst / directives / misc.py
blob5c87edb6e5933565093bde9d40136c7cc0714139
1 # $Id$
2 # Authors: David Goodger <goodger@python.org>; Dethe Elza
3 # Copyright: This module has been placed in the public domain.
5 """Miscellaneous directives."""
7 __docformat__ = 'reStructuredText'
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.parsers.rst import Directive, convert_directive_function
16 from docutils.parsers.rst import directives, roles, states
17 from docutils.parsers.rst.directives.body import CodeBlock, NumberLines
18 from docutils.parsers.rst.roles import set_classes
19 from docutils.transforms import misc
21 class Include(Directive):
23 """
24 Include content read from a separate source file.
26 Content may be parsed by the parser, or included as a literal
27 block. The encoding of the included file can be specified. Only
28 a part of the given file argument may be included by specifying
29 start and end line or text to match before and/or after the text
30 to be used.
31 """
33 required_arguments = 1
34 optional_arguments = 0
35 final_argument_whitespace = True
36 option_spec = {'literal': directives.flag,
37 'code': directives.unchanged,
38 'encoding': directives.encoding,
39 'tab-width': int,
40 'start-line': int,
41 'end-line': int,
42 'start-after': directives.unchanged_required,
43 'end-before': directives.unchanged_required,
44 # ignored except for 'literal' or 'code':
45 'number-lines': directives.unchanged, # integer or None
46 'class': directives.class_option,
47 'name': directives.unchanged}
49 standard_include_path = os.path.join(os.path.dirname(states.__file__),
50 'include')
52 def run(self):
53 """Include a file as part of the content of this reST file."""
54 if not self.state.document.settings.file_insertion_enabled:
55 raise self.warning('"%s" directive disabled.' % self.name)
56 source = self.state_machine.input_lines.source(
57 self.lineno - self.state_machine.input_offset - 1)
58 source_dir = os.path.dirname(os.path.abspath(source))
59 path = directives.path(self.arguments[0])
60 if path.startswith('<') and path.endswith('>'):
61 path = os.path.join(self.standard_include_path, path[1:-1])
62 path = os.path.normpath(os.path.join(source_dir, path))
63 path = utils.relative_path(None, path)
64 path = nodes.reprunicode(path)
65 encoding = self.options.get(
66 'encoding', self.state.document.settings.input_encoding)
67 tab_width = self.options.get(
68 'tab-width', self.state.document.settings.tab_width)
69 try:
70 self.state.document.settings.record_dependencies.add(path)
71 include_file = io.FileInput(
72 source_path=path, encoding=encoding,
73 error_handler=(self.state.document.settings.\
74 input_encoding_error_handler),
75 handle_io_errors=None)
76 except UnicodeEncodeError, 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, 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, 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]
114 include_lines = statemachine.string2lines(rawtext, tab_width,
115 convert_whitespace=True)
116 if 'literal' in self.options:
117 # 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, value)
142 else:
143 literal_block += nodes.Text(text, text)
144 return [literal_block]
145 if 'code' in self.options:
146 self.options['source'] = path
147 codeblock = CodeBlock(self.name,
148 [self.options.pop('code')], # arguments
149 self.options,
150 include_lines, # content
151 self.lineno,
152 self.content_offset,
153 self.block_text,
154 self.state,
155 self.state_machine)
156 return codeblock.run()
157 self.state_machine.insert_input(include_lines, path)
158 return []
161 class Raw(Directive):
164 Pass through content unchanged
166 Content is included in output based on type argument
168 Content may be included inline (content section of directive) or
169 imported from a file or url.
172 required_arguments = 1
173 optional_arguments = 0
174 final_argument_whitespace = True
175 option_spec = {'file': directives.path,
176 'url': directives.uri,
177 'encoding': directives.encoding}
178 has_content = True
180 def run(self):
181 if (not self.state.document.settings.raw_enabled
182 or (not self.state.document.settings.file_insertion_enabled
183 and ('file' in self.options
184 or 'url' in self.options))):
185 raise self.warning('"%s" directive disabled.' % self.name)
186 attributes = {'format': ' '.join(self.arguments[0].lower().split())}
187 encoding = self.options.get(
188 'encoding', self.state.document.settings.input_encoding)
189 if self.content:
190 if 'file' in self.options or 'url' in self.options:
191 raise self.error(
192 '"%s" directive may not both specify an external file '
193 'and have content.' % self.name)
194 text = '\n'.join(self.content)
195 elif 'file' in self.options:
196 if 'url' in self.options:
197 raise self.error(
198 'The "file" and "url" options may not be simultaneously '
199 'specified for the "%s" directive.' % self.name)
200 source_dir = os.path.dirname(
201 os.path.abspath(self.state.document.current_source))
202 path = os.path.normpath(os.path.join(source_dir,
203 self.options['file']))
204 path = utils.relative_path(None, path)
205 try:
206 raw_file = io.FileInput(
207 source_path=path, encoding=encoding,
208 error_handler=(self.state.document.settings.\
209 input_encoding_error_handler),
210 handle_io_errors=None)
211 # TODO: currently, raw input files are recorded as
212 # dependencies even if not used for the chosen output format.
213 self.state.document.settings.record_dependencies.add(path)
214 except IOError, error:
215 raise self.severe(u'Problems with "%s" directive path:\n%s.'
216 % (self.name, ErrorString(error)))
217 try:
218 text = raw_file.read()
219 except UnicodeError, error:
220 raise self.severe(u'Problem with "%s" directive:\n%s'
221 % (self.name, ErrorString(error)))
222 attributes['source'] = path
223 elif 'url' in self.options:
224 source = self.options['url']
225 # Do not import urllib2 at the top of the module because
226 # it may fail due to broken SSL dependencies, and it takes
227 # about 0.15 seconds to load.
228 import urllib2
229 try:
230 raw_text = urllib2.urlopen(source).read()
231 except (urllib2.URLError, IOError, OSError), error:
232 raise self.severe(u'Problems with "%s" directive URL "%s":\n%s.'
233 % (self.name, self.options['url'], ErrorString(error)))
234 raw_file = io.StringInput(
235 source=raw_text, source_path=source, encoding=encoding,
236 error_handler=(self.state.document.settings.\
237 input_encoding_error_handler))
238 try:
239 text = raw_file.read()
240 except UnicodeError, error:
241 raise self.severe(u'Problem with "%s" directive:\n%s'
242 % (self.name, ErrorString(error)))
243 attributes['source'] = source
244 else:
245 # This will always fail because there is no content.
246 self.assert_has_content()
247 raw_node = nodes.raw('', text, **attributes)
248 (raw_node.source,
249 raw_node.line) = self.state_machine.get_source_and_line(self.lineno)
250 return [raw_node]
253 class Replace(Directive):
255 has_content = True
257 def run(self):
258 if not isinstance(self.state, states.SubstitutionDef):
259 raise self.error(
260 'Invalid context: the "%s" directive can only be used within '
261 'a substitution definition.' % self.name)
262 self.assert_has_content()
263 text = '\n'.join(self.content)
264 element = nodes.Element(text)
265 self.state.nested_parse(self.content, self.content_offset,
266 element)
267 # element might contain [paragraph] + system_message(s)
268 node = None
269 messages = []
270 for elem in element:
271 if not node and isinstance(elem, nodes.paragraph):
272 node = elem
273 elif isinstance(elem, nodes.system_message):
274 elem['backrefs'] = []
275 messages.append(elem)
276 else:
277 return [
278 self.state_machine.reporter.error(
279 'Error in "%s" directive: may contain a single paragraph '
280 'only.' % (self.name), line=self.lineno) ]
281 if node:
282 return messages + node.children
283 return messages
285 class Unicode(Directive):
287 r"""
288 Convert Unicode character codes (numbers) to characters. Codes may be
289 decimal numbers, hexadecimal numbers (prefixed by ``0x``, ``x``, ``\x``,
290 ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style numeric character
291 entities (e.g. ``&#x262E;``). Text following ".." is a comment and is
292 ignored. Spaces are ignored, and any other text remains as-is.
295 required_arguments = 1
296 optional_arguments = 0
297 final_argument_whitespace = True
298 option_spec = {'trim': directives.flag,
299 'ltrim': directives.flag,
300 'rtrim': directives.flag}
302 comment_pattern = re.compile(r'( |\n|^)\.\. ')
304 def run(self):
305 if not isinstance(self.state, states.SubstitutionDef):
306 raise self.error(
307 'Invalid context: the "%s" directive can only be used within '
308 'a substitution definition.' % self.name)
309 substitution_definition = self.state_machine.node
310 if 'trim' in self.options:
311 substitution_definition.attributes['ltrim'] = 1
312 substitution_definition.attributes['rtrim'] = 1
313 if 'ltrim' in self.options:
314 substitution_definition.attributes['ltrim'] = 1
315 if 'rtrim' in self.options:
316 substitution_definition.attributes['rtrim'] = 1
317 codes = self.comment_pattern.split(self.arguments[0])[0].split()
318 element = nodes.Element()
319 for code in codes:
320 try:
321 decoded = directives.unicode_code(code)
322 except ValueError, error:
323 raise self.error(u'Invalid character code: %s\n%s'
324 % (code, ErrorString(error)))
325 element += nodes.Text(decoded)
326 return element.children
329 class Class(Directive):
332 Set a "class" attribute on the directive content or the next element.
333 When applied to the next element, a "pending" element is inserted, and a
334 transform does the work later.
337 required_arguments = 1
338 optional_arguments = 0
339 final_argument_whitespace = True
340 has_content = True
342 def run(self):
343 try:
344 class_value = directives.class_option(self.arguments[0])
345 except ValueError:
346 raise self.error(
347 'Invalid class attribute value for "%s" directive: "%s".'
348 % (self.name, self.arguments[0]))
349 node_list = []
350 if self.content:
351 container = nodes.Element()
352 self.state.nested_parse(self.content, self.content_offset,
353 container)
354 for node in container:
355 node['classes'].extend(class_value)
356 node_list.extend(container.children)
357 else:
358 pending = nodes.pending(
359 misc.ClassAttribute,
360 {'class': class_value, 'directive': self.name},
361 self.block_text)
362 self.state_machine.document.note_pending(pending)
363 node_list.append(pending)
364 return node_list
367 class Role(Directive):
369 has_content = True
371 argument_pattern = re.compile(r'(%s)\s*(\(\s*(%s)\s*\)\s*)?$'
372 % ((states.Inliner.simplename,) * 2))
374 def run(self):
375 """Dynamically create and register a custom interpreted text role."""
376 if self.content_offset > self.lineno or not self.content:
377 raise self.error('"%s" directive requires arguments on the first '
378 'line.' % self.name)
379 args = self.content[0]
380 match = self.argument_pattern.match(args)
381 if not match:
382 raise self.error('"%s" directive arguments not valid role names: '
383 '"%s".' % (self.name, args))
384 new_role_name = match.group(1)
385 base_role_name = match.group(3)
386 messages = []
387 if base_role_name:
388 base_role, messages = roles.role(
389 base_role_name, self.state_machine.language, self.lineno,
390 self.state.reporter)
391 if base_role is None:
392 error = self.state.reporter.error(
393 'Unknown interpreted text role "%s".' % base_role_name,
394 nodes.literal_block(self.block_text, self.block_text),
395 line=self.lineno)
396 return messages + [error]
397 else:
398 base_role = roles.generic_custom_role
399 assert not hasattr(base_role, 'arguments'), (
400 'Supplemental directive arguments for "%s" directive not '
401 'supported (specified by "%r" role).' % (self.name, base_role))
402 try:
403 converted_role = convert_directive_function(base_role)
404 (arguments, options, content, content_offset) = (
405 self.state.parse_directive_block(
406 self.content[1:], self.content_offset, converted_role,
407 option_presets={}))
408 except states.MarkupError, detail:
409 error = self.state_machine.reporter.error(
410 'Error in "%s" directive:\n%s.' % (self.name, detail),
411 nodes.literal_block(self.block_text, self.block_text),
412 line=self.lineno)
413 return messages + [error]
414 if 'class' not in options:
415 try:
416 options['class'] = directives.class_option(new_role_name)
417 except ValueError, detail:
418 error = self.state_machine.reporter.error(
419 u'Invalid argument for "%s" directive:\n%s.'
420 % (self.name, SafeString(detail)), nodes.literal_block(
421 self.block_text, self.block_text), line=self.lineno)
422 return messages + [error]
423 role = roles.CustomRole(new_role_name, base_role, options, content)
424 roles.register_local_role(new_role_name, role)
425 return messages
428 class DefaultRole(Directive):
430 """Set the default interpreted text role."""
432 optional_arguments = 1
433 final_argument_whitespace = False
435 def run(self):
436 if not self.arguments:
437 if '' in roles._roles:
438 # restore the "default" default role
439 del roles._roles['']
440 return []
441 role_name = self.arguments[0]
442 role, messages = roles.role(role_name, self.state_machine.language,
443 self.lineno, self.state.reporter)
444 if role is None:
445 error = self.state.reporter.error(
446 'Unknown interpreted text role "%s".' % role_name,
447 nodes.literal_block(self.block_text, self.block_text),
448 line=self.lineno)
449 return messages + [error]
450 roles._roles[''] = role
451 # @@@ should this be local to the document, not the parser?
452 return messages
455 class Title(Directive):
457 required_arguments = 1
458 optional_arguments = 0
459 final_argument_whitespace = True
461 def run(self):
462 self.state_machine.document['title'] = self.arguments[0]
463 return []
466 class Date(Directive):
468 has_content = True
470 def run(self):
471 if not isinstance(self.state, states.SubstitutionDef):
472 raise self.error(
473 'Invalid context: the "%s" directive can only be used within '
474 'a substitution definition.' % self.name)
475 format = '\n'.join(self.content) or '%Y-%m-%d'
476 text = time.strftime(format)
477 return [nodes.Text(text)]
480 class TestDirective(Directive):
482 """This directive is useful only for testing purposes."""
484 optional_arguments = 1
485 final_argument_whitespace = True
486 option_spec = {'option': directives.unchanged_required}
487 has_content = True
489 def run(self):
490 if self.content:
491 text = '\n'.join(self.content)
492 info = self.state_machine.reporter.info(
493 'Directive processed. Type="%s", arguments=%r, options=%r, '
494 'content:' % (self.name, self.arguments, self.options),
495 nodes.literal_block(text, text), line=self.lineno)
496 else:
497 info = self.state_machine.reporter.info(
498 'Directive processed. Type="%s", arguments=%r, options=%r, '
499 'content: None' % (self.name, self.arguments, self.options),
500 line=self.lineno)
501 return [info]
503 # Old-style, functional definition:
505 # def directive_test_function(name, arguments, options, content, lineno,
506 # content_offset, block_text, state, state_machine):
507 # """This directive is useful only for testing purposes."""
508 # if content:
509 # text = '\n'.join(content)
510 # info = state_machine.reporter.info(
511 # 'Directive processed. Type="%s", arguments=%r, options=%r, '
512 # 'content:' % (name, arguments, options),
513 # nodes.literal_block(text, text), line=lineno)
514 # else:
515 # info = state_machine.reporter.info(
516 # 'Directive processed. Type="%s", arguments=%r, options=%r, '
517 # 'content: None' % (name, arguments, options), line=lineno)
518 # return [info]
520 # directive_test_function.arguments = (0, 1, 1)
521 # directive_test_function.options = {'option': directives.unchanged_required}
522 # directive_test_function.content = 1