Clean up system message (source, line) reporting.
[docutils.git] / docutils / parsers / rst / directives / misc.py
blobcc5a81f6cf70bdd86d65c2864253dda176840cee
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.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 IOError, error:
77 raise self.severe(u'Problems with "%s" directive path:\n%s.' %
78 (self.name, ErrorString(error)))
79 startline = self.options.get('start-line', None)
80 endline = self.options.get('end-line', None)
81 try:
82 if startline or (endline is not None):
83 lines = include_file.readlines()
84 rawtext = ''.join(lines[startline:endline])
85 else:
86 rawtext = include_file.read()
87 except UnicodeError, error:
88 raise self.severe(u'Problem with "%s" directive:\n%s' %
89 (self.name, ErrorString(error)))
90 # start-after/end-before: no restrictions on newlines in match-text,
91 # and no restrictions on matching inside lines vs. line boundaries
92 after_text = self.options.get('start-after', None)
93 if after_text:
94 # skip content in rawtext before *and incl.* a matching text
95 after_index = rawtext.find(after_text)
96 if after_index < 0:
97 raise self.severe('Problem with "start-after" option of "%s" '
98 'directive:\nText not found.' % self.name)
99 rawtext = rawtext[after_index + len(after_text):]
100 before_text = self.options.get('end-before', None)
101 if before_text:
102 # skip content in rawtext after *and incl.* a matching text
103 before_index = rawtext.find(before_text)
104 if before_index < 0:
105 raise self.severe('Problem with "end-before" option of "%s" '
106 'directive:\nText not found.' % self.name)
107 rawtext = rawtext[:before_index]
109 include_lines = statemachine.string2lines(rawtext, tab_width,
110 convert_whitespace=1)
111 if 'literal' in self.options:
112 # Convert tabs to spaces, if `tab_width` is positive.
113 if tab_width >= 0:
114 text = rawtext.expandtabs(tab_width)
115 else:
116 text = rawtext
117 literal_block = nodes.literal_block(rawtext, source=path,
118 classes=self.options.get('class', []))
119 literal_block.line = 1
120 self.add_name(literal_block)
121 if 'number-lines' in self.options:
122 try:
123 startline = int(self.options['number-lines'] or 1)
124 except ValueError:
125 raise self.error(':number-lines: with non-integer '
126 'start value')
127 endline = startline + len(include_lines)
128 if text.endswith('\n'):
129 text = text[:-1]
130 tokens = NumberLines([([], text)], startline, endline)
131 for classes, value in tokens:
132 if classes:
133 literal_block += nodes.inline(value, value,
134 classes=classes)
135 else:
136 literal_block += nodes.Text(value, value)
137 else:
138 literal_block += nodes.Text(text, text)
139 return [literal_block]
140 if 'code' in self.options:
141 self.options['source'] = path
142 codeblock = CodeBlock(self.name,
143 [self.options.pop('code')], # arguments
144 self.options,
145 include_lines, # content
146 self.lineno,
147 self.content_offset,
148 self.block_text,
149 self.state,
150 self.state_machine)
151 return codeblock.run()
152 self.state_machine.insert_input(include_lines, path)
153 return []
156 class Raw(Directive):
159 Pass through content unchanged
161 Content is included in output based on type argument
163 Content may be included inline (content section of directive) or
164 imported from a file or url.
167 required_arguments = 1
168 optional_arguments = 0
169 final_argument_whitespace = True
170 option_spec = {'file': directives.path,
171 'url': directives.uri,
172 'encoding': directives.encoding}
173 has_content = True
175 def run(self):
176 if (not self.state.document.settings.raw_enabled
177 or (not self.state.document.settings.file_insertion_enabled
178 and ('file' in self.options
179 or 'url' in self.options))):
180 raise self.warning('"%s" directive disabled.' % self.name)
181 attributes = {'format': ' '.join(self.arguments[0].lower().split())}
182 encoding = self.options.get(
183 'encoding', self.state.document.settings.input_encoding)
184 if self.content:
185 if 'file' in self.options or 'url' in self.options:
186 raise self.error(
187 '"%s" directive may not both specify an external file '
188 'and have content.' % self.name)
189 text = '\n'.join(self.content)
190 elif 'file' in self.options:
191 if 'url' in self.options:
192 raise self.error(
193 'The "file" and "url" options may not be simultaneously '
194 'specified for the "%s" directive.' % self.name)
195 source_dir = os.path.dirname(
196 os.path.abspath(self.state.document.current_source))
197 path = os.path.normpath(os.path.join(source_dir,
198 self.options['file']))
199 path = utils.relative_path(None, path)
200 try:
201 raw_file = io.FileInput(
202 source_path=path, encoding=encoding,
203 error_handler=(self.state.document.settings.\
204 input_encoding_error_handler),
205 handle_io_errors=None)
206 # TODO: currently, raw input files are recorded as
207 # dependencies even if not used for the chosen output format.
208 self.state.document.settings.record_dependencies.add(path)
209 except IOError, error:
210 raise self.severe(u'Problems with "%s" directive path:\n%s.'
211 % (self.name, ErrorString(error)))
212 try:
213 text = raw_file.read()
214 except UnicodeError, error:
215 raise self.severe(u'Problem with "%s" directive:\n%s'
216 % (self.name, ErrorString(error)))
217 attributes['source'] = path
218 elif 'url' in self.options:
219 source = self.options['url']
220 # Do not import urllib2 at the top of the module because
221 # it may fail due to broken SSL dependencies, and it takes
222 # about 0.15 seconds to load.
223 import urllib2
224 try:
225 raw_text = urllib2.urlopen(source).read()
226 except (urllib2.URLError, IOError, OSError), error:
227 raise self.severe(u'Problems with "%s" directive URL "%s":\n%s.'
228 % (self.name, self.options['url'], ErrorString(error)))
229 raw_file = io.StringInput(
230 source=raw_text, source_path=source, encoding=encoding,
231 error_handler=(self.state.document.settings.\
232 input_encoding_error_handler))
233 try:
234 text = raw_file.read()
235 except UnicodeError, error:
236 raise self.severe(u'Problem with "%s" directive:\n%s'
237 % (self.name, ErrorString(error)))
238 attributes['source'] = source
239 else:
240 # This will always fail because there is no content.
241 self.assert_has_content()
242 raw_node = nodes.raw('', text, **attributes)
243 (raw_node.source,
244 raw_node.line) = self.state_machine.get_source_and_line(self.lineno)
245 return [raw_node]
248 class Replace(Directive):
250 has_content = True
252 def run(self):
253 if not isinstance(self.state, states.SubstitutionDef):
254 raise self.error(
255 'Invalid context: the "%s" directive can only be used within '
256 'a substitution definition.' % self.name)
257 self.assert_has_content()
258 text = '\n'.join(self.content)
259 element = nodes.Element(text)
260 self.state.nested_parse(self.content, self.content_offset,
261 element)
262 # element might contain [paragraph] + system_message(s)
263 node = None
264 messages = []
265 for elem in element:
266 if not node and isinstance(elem, nodes.paragraph):
267 node = elem
268 elif isinstance(elem, nodes.system_message):
269 elem['backrefs'] = []
270 messages.append(elem)
271 else:
272 return [
273 self.state_machine.reporter.error(
274 'Error in "%s" directive: may contain a single paragraph '
275 'only.' % (self.name), line=self.lineno) ]
276 if node:
277 return messages + node.children
278 return messages
280 class Unicode(Directive):
282 r"""
283 Convert Unicode character codes (numbers) to characters. Codes may be
284 decimal numbers, hexadecimal numbers (prefixed by ``0x``, ``x``, ``\x``,
285 ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style numeric character
286 entities (e.g. ``&#x262E;``). Text following ".." is a comment and is
287 ignored. Spaces are ignored, and any other text remains as-is.
290 required_arguments = 1
291 optional_arguments = 0
292 final_argument_whitespace = True
293 option_spec = {'trim': directives.flag,
294 'ltrim': directives.flag,
295 'rtrim': directives.flag}
297 comment_pattern = re.compile(r'( |\n|^)\.\. ')
299 def run(self):
300 if not isinstance(self.state, states.SubstitutionDef):
301 raise self.error(
302 'Invalid context: the "%s" directive can only be used within '
303 'a substitution definition.' % self.name)
304 substitution_definition = self.state_machine.node
305 if 'trim' in self.options:
306 substitution_definition.attributes['ltrim'] = 1
307 substitution_definition.attributes['rtrim'] = 1
308 if 'ltrim' in self.options:
309 substitution_definition.attributes['ltrim'] = 1
310 if 'rtrim' in self.options:
311 substitution_definition.attributes['rtrim'] = 1
312 codes = self.comment_pattern.split(self.arguments[0])[0].split()
313 element = nodes.Element()
314 for code in codes:
315 try:
316 decoded = directives.unicode_code(code)
317 except ValueError, error:
318 raise self.error(u'Invalid character code: %s\n%s'
319 % (code, ErrorString(error)))
320 element += nodes.Text(decoded)
321 return element.children
324 class Class(Directive):
327 Set a "class" attribute on the directive content or the next element.
328 When applied to the next element, a "pending" element is inserted, and a
329 transform does the work later.
332 required_arguments = 1
333 optional_arguments = 0
334 final_argument_whitespace = True
335 has_content = True
337 def run(self):
338 try:
339 class_value = directives.class_option(self.arguments[0])
340 except ValueError:
341 raise self.error(
342 'Invalid class attribute value for "%s" directive: "%s".'
343 % (self.name, self.arguments[0]))
344 node_list = []
345 if self.content:
346 container = nodes.Element()
347 self.state.nested_parse(self.content, self.content_offset,
348 container)
349 for node in container:
350 node['classes'].extend(class_value)
351 node_list.extend(container.children)
352 else:
353 pending = nodes.pending(
354 misc.ClassAttribute,
355 {'class': class_value, 'directive': self.name},
356 self.block_text)
357 self.state_machine.document.note_pending(pending)
358 node_list.append(pending)
359 return node_list
362 class Role(Directive):
364 has_content = True
366 argument_pattern = re.compile(r'(%s)\s*(\(\s*(%s)\s*\)\s*)?$'
367 % ((states.Inliner.simplename,) * 2))
369 def run(self):
370 """Dynamically create and register a custom interpreted text role."""
371 if self.content_offset > self.lineno or not self.content:
372 raise self.error('"%s" directive requires arguments on the first '
373 'line.' % self.name)
374 args = self.content[0]
375 match = self.argument_pattern.match(args)
376 if not match:
377 raise self.error('"%s" directive arguments not valid role names: '
378 '"%s".' % (self.name, args))
379 new_role_name = match.group(1)
380 base_role_name = match.group(3)
381 messages = []
382 if base_role_name:
383 base_role, messages = roles.role(
384 base_role_name, self.state_machine.language, self.lineno,
385 self.state.reporter)
386 if base_role is None:
387 error = self.state.reporter.error(
388 'Unknown interpreted text role "%s".' % base_role_name,
389 nodes.literal_block(self.block_text, self.block_text),
390 line=self.lineno)
391 return messages + [error]
392 else:
393 base_role = roles.generic_custom_role
394 assert not hasattr(base_role, 'arguments'), (
395 'Supplemental directive arguments for "%s" directive not '
396 'supported (specified by "%r" role).' % (self.name, base_role))
397 try:
398 converted_role = convert_directive_function(base_role)
399 (arguments, options, content, content_offset) = (
400 self.state.parse_directive_block(
401 self.content[1:], self.content_offset, converted_role,
402 option_presets={}))
403 except states.MarkupError, detail:
404 error = self.state_machine.reporter.error(
405 'Error in "%s" directive:\n%s.' % (self.name, detail),
406 nodes.literal_block(self.block_text, self.block_text),
407 line=self.lineno)
408 return messages + [error]
409 if 'class' not in options:
410 try:
411 options['class'] = directives.class_option(new_role_name)
412 except ValueError, detail:
413 error = self.state_machine.reporter.error(
414 u'Invalid argument for "%s" directive:\n%s.'
415 % (self.name, SafeString(detail)), nodes.literal_block(
416 self.block_text, self.block_text), line=self.lineno)
417 return messages + [error]
418 role = roles.CustomRole(new_role_name, base_role, options, content)
419 roles.register_local_role(new_role_name, role)
420 return messages
423 class DefaultRole(Directive):
425 """Set the default interpreted text role."""
427 optional_arguments = 1
428 final_argument_whitespace = False
430 def run(self):
431 if not self.arguments:
432 if '' in roles._roles:
433 # restore the "default" default role
434 del roles._roles['']
435 return []
436 role_name = self.arguments[0]
437 role, messages = roles.role(role_name, self.state_machine.language,
438 self.lineno, self.state.reporter)
439 if role is None:
440 error = self.state.reporter.error(
441 'Unknown interpreted text role "%s".' % role_name,
442 nodes.literal_block(self.block_text, self.block_text),
443 line=self.lineno)
444 return messages + [error]
445 roles._roles[''] = role
446 # @@@ should this be local to the document, not the parser?
447 return messages
450 class Title(Directive):
452 required_arguments = 1
453 optional_arguments = 0
454 final_argument_whitespace = True
456 def run(self):
457 self.state_machine.document['title'] = self.arguments[0]
458 return []
461 class Date(Directive):
463 has_content = True
465 def run(self):
466 if not isinstance(self.state, states.SubstitutionDef):
467 raise self.error(
468 'Invalid context: the "%s" directive can only be used within '
469 'a substitution definition.' % self.name)
470 format = '\n'.join(self.content) or '%Y-%m-%d'
471 text = time.strftime(format)
472 return [nodes.Text(text)]
475 class TestDirective(Directive):
477 """This directive is useful only for testing purposes."""
479 optional_arguments = 1
480 final_argument_whitespace = True
481 option_spec = {'option': directives.unchanged_required}
482 has_content = True
484 def run(self):
485 if self.content:
486 text = '\n'.join(self.content)
487 info = self.state_machine.reporter.info(
488 'Directive processed. Type="%s", arguments=%r, options=%r, '
489 'content:' % (self.name, self.arguments, self.options),
490 nodes.literal_block(text, text), line=self.lineno)
491 else:
492 info = self.state_machine.reporter.info(
493 'Directive processed. Type="%s", arguments=%r, options=%r, '
494 'content: None' % (self.name, self.arguments, self.options),
495 line=self.lineno)
496 return [info]
498 # Old-style, functional definition:
500 # def directive_test_function(name, arguments, options, content, lineno,
501 # content_offset, block_text, state, state_machine):
502 # """This directive is useful only for testing purposes."""
503 # if content:
504 # text = '\n'.join(content)
505 # info = state_machine.reporter.info(
506 # 'Directive processed. Type="%s", arguments=%r, options=%r, '
507 # 'content:' % (name, arguments, options),
508 # nodes.literal_block(text, text), line=lineno)
509 # else:
510 # info = state_machine.reporter.info(
511 # 'Directive processed. Type="%s", arguments=%r, options=%r, '
512 # 'content: None' % (name, arguments, options), line=lineno)
513 # return [info]
515 # directive_test_function.arguments = (0, 1, 1)
516 # directive_test_function.options = {'option': directives.unchanged_required}
517 # directive_test_function.content = 1