Common directive options:
[docutils.git] / docutils / parsers / rst / directives / misc.py
blobb68446bb40426c2cf8dc13dc60d876b875e54c9a
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.transforms import misc
19 class Include(Directive):
21 """
22 Include content read from a separate source file.
24 Content may be parsed by the parser, or included as a literal
25 block. The encoding of the included file can be specified. Only
26 a part of the given file argument may be included by specifying
27 start and end line or text to match before and/or after the text
28 to be used.
29 """
31 required_arguments = 1
32 optional_arguments = 0
33 final_argument_whitespace = True
34 option_spec = {'literal': directives.flag,
35 'encoding': directives.encoding,
36 'tab-width': int,
37 'start-line': int,
38 'end-line': int,
39 'start-after': directives.unchanged_required,
40 'end-before': directives.unchanged_required}
42 standard_include_path = os.path.join(os.path.dirname(states.__file__),
43 'include')
45 def run(self):
46 """Include a reST file as part of the content of this reST file."""
47 if not self.state.document.settings.file_insertion_enabled:
48 raise self.warning('"%s" directive disabled.' % self.name)
49 source = self.state_machine.input_lines.source(
50 self.lineno - self.state_machine.input_offset - 1)
51 source_dir = os.path.dirname(os.path.abspath(source))
52 path = directives.path(self.arguments[0])
53 if path.startswith('<') and path.endswith('>'):
54 path = os.path.join(self.standard_include_path, path[1:-1])
55 path = os.path.normpath(os.path.join(source_dir, path))
56 path = utils.relative_path(None, path)
57 path = nodes.reprunicode(path)
58 encoding = self.options.get(
59 'encoding', self.state.document.settings.input_encoding)
60 tab_width = self.options.get(
61 'tab-width', self.state.document.settings.tab_width)
62 try:
63 self.state.document.settings.record_dependencies.add(path)
64 include_file = io.FileInput(
65 source_path=path, encoding=encoding,
66 error_handler=(self.state.document.settings.\
67 input_encoding_error_handler),
68 handle_io_errors=None)
69 except IOError, error:
70 raise self.severe(u'Problems with "%s" directive path:\n%s.' %
71 (self.name, ErrorString(error)))
72 startline = self.options.get('start-line', None)
73 endline = self.options.get('end-line', None)
74 try:
75 if startline or (endline is not None):
76 lines = include_file.readlines()
77 rawtext = ''.join(lines[startline:endline])
78 else:
79 rawtext = include_file.read()
80 except UnicodeError, error:
81 raise self.severe(u'Problem with "%s" directive:\n%s' %
82 (self.name, ErrorString(error)))
83 # start-after/end-before: no restrictions on newlines in match-text,
84 # and no restrictions on matching inside lines vs. line boundaries
85 after_text = self.options.get('start-after', None)
86 if after_text:
87 # skip content in rawtext before *and incl.* a matching text
88 after_index = rawtext.find(after_text)
89 if after_index < 0:
90 raise self.severe('Problem with "start-after" option of "%s" '
91 'directive:\nText not found.' % self.name)
92 rawtext = rawtext[after_index + len(after_text):]
93 before_text = self.options.get('end-before', None)
94 if before_text:
95 # skip content in rawtext after *and incl.* a matching text
96 before_index = rawtext.find(before_text)
97 if before_index < 0:
98 raise self.severe('Problem with "end-before" option of "%s" '
99 'directive:\nText not found.' % self.name)
100 rawtext = rawtext[:before_index]
101 if 'literal' in self.options:
102 # Convert tabs to spaces, if `tab_width` is positive.
103 if tab_width >= 0:
104 text = rawtext.expandtabs(tab_width)
105 else:
106 text = rawtext
107 literal_block = nodes.literal_block(rawtext, text, source=path)
108 literal_block.line = 1
109 return [literal_block]
110 else:
111 include_lines = statemachine.string2lines(
112 rawtext, tab_width, convert_whitespace=1)
113 self.state_machine.insert_input(include_lines, path)
114 return []
117 class Raw(Directive):
120 Pass through content unchanged
122 Content is included in output based on type argument
124 Content may be included inline (content section of directive) or
125 imported from a file or url.
128 required_arguments = 1
129 optional_arguments = 0
130 final_argument_whitespace = True
131 option_spec = {'file': directives.path,
132 'url': directives.uri,
133 'encoding': directives.encoding}
134 has_content = True
136 def run(self):
137 if (not self.state.document.settings.raw_enabled
138 or (not self.state.document.settings.file_insertion_enabled
139 and ('file' in self.options
140 or 'url' in self.options))):
141 raise self.warning('"%s" directive disabled.' % self.name)
142 attributes = {'format': ' '.join(self.arguments[0].lower().split())}
143 encoding = self.options.get(
144 'encoding', self.state.document.settings.input_encoding)
145 if self.content:
146 if 'file' in self.options or 'url' in self.options:
147 raise self.error(
148 '"%s" directive may not both specify an external file '
149 'and have content.' % self.name)
150 text = '\n'.join(self.content)
151 elif 'file' in self.options:
152 if 'url' in self.options:
153 raise self.error(
154 'The "file" and "url" options may not be simultaneously '
155 'specified for the "%s" directive.' % self.name)
156 source_dir = os.path.dirname(
157 os.path.abspath(self.state.document.current_source))
158 path = os.path.normpath(os.path.join(source_dir,
159 self.options['file']))
160 path = utils.relative_path(None, path)
161 try:
162 self.state.document.settings.record_dependencies.add(path)
163 raw_file = io.FileInput(
164 source_path=path, encoding=encoding,
165 error_handler=(self.state.document.settings.\
166 input_encoding_error_handler),
167 handle_io_errors=None)
168 except IOError, error:
169 raise self.severe(u'Problems with "%s" directive path:\n%s.'
170 % (self.name, ErrorString(error)))
171 try:
172 text = raw_file.read()
173 except UnicodeError, error:
174 raise self.severe(u'Problem with "%s" directive:\n%s'
175 % (self.name, ErrorString(error)))
176 attributes['source'] = path
177 elif 'url' in self.options:
178 source = self.options['url']
179 # Do not import urllib2 at the top of the module because
180 # it may fail due to broken SSL dependencies, and it takes
181 # about 0.15 seconds to load.
182 import urllib2
183 try:
184 raw_text = urllib2.urlopen(source).read()
185 except (urllib2.URLError, IOError, OSError), error:
186 raise self.severe(u'Problems with "%s" directive URL "%s":\n%s.'
187 % (self.name, self.options['url'], ErrorString(error)))
188 raw_file = io.StringInput(
189 source=raw_text, source_path=source, encoding=encoding,
190 error_handler=(self.state.document.settings.\
191 input_encoding_error_handler))
192 try:
193 text = raw_file.read()
194 except UnicodeError, error:
195 raise self.severe(u'Problem with "%s" directive:\n%s'
196 % (self.name, ErrorString(error)))
197 attributes['source'] = source
198 else:
199 # This will always fail because there is no content.
200 self.assert_has_content()
201 raw_node = nodes.raw('', text, **attributes)
202 return [raw_node]
205 class Replace(Directive):
207 has_content = True
209 def run(self):
210 if not isinstance(self.state, states.SubstitutionDef):
211 raise self.error(
212 'Invalid context: the "%s" directive can only be used within '
213 'a substitution definition.' % self.name)
214 self.assert_has_content()
215 text = '\n'.join(self.content)
216 element = nodes.Element(text)
217 self.state.nested_parse(self.content, self.content_offset,
218 element)
219 # element might contain [paragraph] + system_message(s)
220 node = None
221 messages = []
222 for elem in element:
223 if not node and isinstance(elem, nodes.paragraph):
224 node = elem
225 elif isinstance(elem, nodes.system_message):
226 elem['backrefs'] = []
227 messages.append(elem)
228 else:
229 return [
230 self.state_machine.reporter.error(
231 'Error in "%s" directive: may contain a single paragraph '
232 'only.' % (self.name), line=self.lineno) ]
233 if node:
234 return messages + node.children
235 return messages
237 class Unicode(Directive):
239 r"""
240 Convert Unicode character codes (numbers) to characters. Codes may be
241 decimal numbers, hexadecimal numbers (prefixed by ``0x``, ``x``, ``\x``,
242 ``U+``, ``u``, or ``\u``; e.g. ``U+262E``), or XML-style numeric character
243 entities (e.g. ``&#x262E;``). Text following ".." is a comment and is
244 ignored. Spaces are ignored, and any other text remains as-is.
247 required_arguments = 1
248 optional_arguments = 0
249 final_argument_whitespace = True
250 option_spec = {'trim': directives.flag,
251 'ltrim': directives.flag,
252 'rtrim': directives.flag}
254 comment_pattern = re.compile(r'( |\n|^)\.\. ')
256 def run(self):
257 if not isinstance(self.state, states.SubstitutionDef):
258 raise self.error(
259 'Invalid context: the "%s" directive can only be used within '
260 'a substitution definition.' % self.name)
261 substitution_definition = self.state_machine.node
262 if 'trim' in self.options:
263 substitution_definition.attributes['ltrim'] = 1
264 substitution_definition.attributes['rtrim'] = 1
265 if 'ltrim' in self.options:
266 substitution_definition.attributes['ltrim'] = 1
267 if 'rtrim' in self.options:
268 substitution_definition.attributes['rtrim'] = 1
269 codes = self.comment_pattern.split(self.arguments[0])[0].split()
270 element = nodes.Element()
271 for code in codes:
272 try:
273 decoded = directives.unicode_code(code)
274 except ValueError, error:
275 raise self.error(u'Invalid character code: %s\n%s'
276 % (code, ErrorString(error)))
277 element += nodes.Text(decoded)
278 return element.children
281 class Class(Directive):
284 Set a "class" attribute on the directive content or the next element.
285 When applied to the next element, a "pending" element is inserted, and a
286 transform does the work later.
289 required_arguments = 1
290 optional_arguments = 0
291 final_argument_whitespace = True
292 has_content = True
294 def run(self):
295 try:
296 class_value = directives.class_option(self.arguments[0])
297 except ValueError:
298 raise self.error(
299 'Invalid class attribute value for "%s" directive: "%s".'
300 % (self.name, self.arguments[0]))
301 node_list = []
302 if self.content:
303 container = nodes.Element()
304 self.state.nested_parse(self.content, self.content_offset,
305 container)
306 for node in container:
307 node['classes'].extend(class_value)
308 node_list.extend(container.children)
309 else:
310 pending = nodes.pending(
311 misc.ClassAttribute,
312 {'class': class_value, 'directive': self.name},
313 self.block_text)
314 self.state_machine.document.note_pending(pending)
315 node_list.append(pending)
316 return node_list
319 class Role(Directive):
321 has_content = True
323 argument_pattern = re.compile(r'(%s)\s*(\(\s*(%s)\s*\)\s*)?$'
324 % ((states.Inliner.simplename,) * 2))
326 def run(self):
327 """Dynamically create and register a custom interpreted text role."""
328 if self.content_offset > self.lineno or not self.content:
329 raise self.error('"%s" directive requires arguments on the first '
330 'line.' % self.name)
331 args = self.content[0]
332 match = self.argument_pattern.match(args)
333 if not match:
334 raise self.error('"%s" directive arguments not valid role names: '
335 '"%s".' % (self.name, args))
336 new_role_name = match.group(1)
337 base_role_name = match.group(3)
338 messages = []
339 if base_role_name:
340 base_role, messages = roles.role(
341 base_role_name, self.state_machine.language, self.lineno,
342 self.state.reporter)
343 if base_role is None:
344 error = self.state.reporter.error(
345 'Unknown interpreted text role "%s".' % base_role_name,
346 nodes.literal_block(self.block_text, self.block_text),
347 line=self.lineno)
348 return messages + [error]
349 else:
350 base_role = roles.generic_custom_role
351 assert not hasattr(base_role, 'arguments'), (
352 'Supplemental directive arguments for "%s" directive not '
353 'supported (specified by "%r" role).' % (self.name, base_role))
354 try:
355 converted_role = convert_directive_function(base_role)
356 (arguments, options, content, content_offset) = (
357 self.state.parse_directive_block(
358 self.content[1:], self.content_offset, converted_role,
359 option_presets={}))
360 except states.MarkupError, detail:
361 error = self.state_machine.reporter.error(
362 'Error in "%s" directive:\n%s.' % (self.name, detail),
363 nodes.literal_block(self.block_text, self.block_text),
364 line=self.lineno)
365 return messages + [error]
366 if 'class' not in options:
367 try:
368 options['class'] = directives.class_option(new_role_name)
369 except ValueError, detail:
370 error = self.state_machine.reporter.error(
371 u'Invalid argument for "%s" directive:\n%s.'
372 % (self.name, SafeString(detail)), nodes.literal_block(
373 self.block_text, self.block_text), line=self.lineno)
374 return messages + [error]
375 role = roles.CustomRole(new_role_name, base_role, options, content)
376 roles.register_local_role(new_role_name, role)
377 return messages
380 class DefaultRole(Directive):
382 """Set the default interpreted text role."""
384 optional_arguments = 1
385 final_argument_whitespace = False
387 def run(self):
388 if not self.arguments:
389 if '' in roles._roles:
390 # restore the "default" default role
391 del roles._roles['']
392 return []
393 role_name = self.arguments[0]
394 role, messages = roles.role(role_name, self.state_machine.language,
395 self.lineno, self.state.reporter)
396 if role is None:
397 error = self.state.reporter.error(
398 'Unknown interpreted text role "%s".' % role_name,
399 nodes.literal_block(self.block_text, self.block_text),
400 line=self.lineno)
401 return messages + [error]
402 roles._roles[''] = role
403 # @@@ should this be local to the document, not the parser?
404 return messages
407 class Title(Directive):
409 required_arguments = 1
410 optional_arguments = 0
411 final_argument_whitespace = True
413 def run(self):
414 self.state_machine.document['title'] = self.arguments[0]
415 return []
418 class Date(Directive):
420 has_content = True
422 def run(self):
423 if not isinstance(self.state, states.SubstitutionDef):
424 raise self.error(
425 'Invalid context: the "%s" directive can only be used within '
426 'a substitution definition.' % self.name)
427 format = '\n'.join(self.content) or '%Y-%m-%d'
428 text = time.strftime(format)
429 return [nodes.Text(text)]
432 class TestDirective(Directive):
434 """This directive is useful only for testing purposes."""
436 optional_arguments = 1
437 final_argument_whitespace = True
438 option_spec = {'option': directives.unchanged_required}
439 has_content = True
441 def run(self):
442 if self.content:
443 text = '\n'.join(self.content)
444 info = self.state_machine.reporter.info(
445 'Directive processed. Type="%s", arguments=%r, options=%r, '
446 'content:' % (self.name, self.arguments, self.options),
447 nodes.literal_block(text, text), line=self.lineno)
448 else:
449 info = self.state_machine.reporter.info(
450 'Directive processed. Type="%s", arguments=%r, options=%r, '
451 'content: None' % (self.name, self.arguments, self.options),
452 line=self.lineno)
453 return [info]
455 # Old-style, functional definition:
457 # def directive_test_function(name, arguments, options, content, lineno,
458 # content_offset, block_text, state, state_machine):
459 # """This directive is useful only for testing purposes."""
460 # if content:
461 # text = '\n'.join(content)
462 # info = state_machine.reporter.info(
463 # 'Directive processed. Type="%s", arguments=%r, options=%r, '
464 # 'content:' % (name, arguments, options),
465 # nodes.literal_block(text, text), line=lineno)
466 # else:
467 # info = state_machine.reporter.info(
468 # 'Directive processed. Type="%s", arguments=%r, options=%r, '
469 # 'content: None' % (name, arguments, options), line=lineno)
470 # return [info]
472 # directive_test_function.arguments = (0, 1, 1)
473 # directive_test_function.options = {'option': directives.unchanged_required}
474 # directive_test_function.content = 1