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'
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
):
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
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
,
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
__),
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
)
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" '
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)
87 if startline
or (endline
is not None):
88 lines
= include_file
.readlines()
89 rawtext
= ''.join(lines
[startline
:endline
])
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)
99 # skip content in rawtext before *and incl.* a matching text
100 after_index
= rawtext
.find(after_text
)
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)
107 # skip content in rawtext after *and incl.* a matching text
108 before_index
= rawtext
.find(before_text
)
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.
119 text
= rawtext
.expandtabs(tab_width
)
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
:
128 startline
= int(self
.options
['number-lines'] or 1)
130 raise self
.error(':number-lines: with non-integer '
132 endline
= startline
+ len(include_lines
)
133 if text
.endswith('\n'):
135 tokens
= NumberLines([([], text
)], startline
, endline
)
136 for classes
, value
in tokens
:
138 literal_block
+= nodes
.inline(value
, value
,
141 literal_block
+= nodes
.Text(value
, value
)
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
150 include_lines
, # content
156 return codeblock
.run()
157 self
.state_machine
.insert_input(include_lines
, path
)
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
}
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
)
190 if 'file' in self
.options
or 'url' in self
.options
:
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
:
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
)
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
)))
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.
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
))
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
245 # This will always fail because there is no content.
246 self
.assert_has_content()
247 raw_node
= nodes
.raw('', text
, **attributes
)
249 raw_node
.line
) = self
.state_machine
.get_source_and_line(self
.lineno
)
253 class Replace(Directive
):
258 if not isinstance(self
.state
, states
.SubstitutionDef
):
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
,
267 # element might contain [paragraph] + system_message(s)
271 if not node
and isinstance(elem
, nodes
.paragraph
):
273 elif isinstance(elem
, nodes
.system_message
):
274 elem
['backrefs'] = []
275 messages
.append(elem
)
278 self
.state_machine
.reporter
.error(
279 'Error in "%s" directive: may contain a single paragraph '
280 'only.' % (self
.name
), line
=self
.lineno
) ]
282 return messages
+ node
.children
285 class Unicode(Directive
):
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. ``☮``). 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|^)\.\. ')
305 if not isinstance(self
.state
, states
.SubstitutionDef
):
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()
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
344 class_value
= directives
.class_option(self
.arguments
[0])
347 'Invalid class attribute value for "%s" directive: "%s".'
348 % (self
.name
, self
.arguments
[0]))
351 container
= nodes
.Element()
352 self
.state
.nested_parse(self
.content
, self
.content_offset
,
354 for node
in container
:
355 node
['classes'].extend(class_value
)
356 node_list
.extend(container
.children
)
358 pending
= nodes
.pending(
360 {'class': class_value
, 'directive': self
.name
},
362 self
.state_machine
.document
.note_pending(pending
)
363 node_list
.append(pending
)
367 class Role(Directive
):
371 argument_pattern
= re
.compile(r
'(%s)\s*(\(\s*(%s)\s*\)\s*)?$'
372 % ((states
.Inliner
.simplename
,) * 2))
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 '
379 args
= self
.content
[0]
380 match
= self
.argument_pattern
.match(args
)
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)
388 base_role
, messages
= roles
.role(
389 base_role_name
, self
.state_machine
.language
, self
.lineno
,
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
),
396 return messages
+ [error
]
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
))
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
,
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
),
413 return messages
+ [error
]
414 if 'class' not in options
:
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
)
428 class DefaultRole(Directive
):
430 """Set the default interpreted text role."""
432 optional_arguments
= 1
433 final_argument_whitespace
= False
436 if not self
.arguments
:
437 if '' in roles
._roles
:
438 # restore the "default" default role
441 role_name
= self
.arguments
[0]
442 role
, messages
= roles
.role(role_name
, self
.state_machine
.language
,
443 self
.lineno
, self
.state
.reporter
)
445 error
= self
.state
.reporter
.error(
446 'Unknown interpreted text role "%s".' % role_name
,
447 nodes
.literal_block(self
.block_text
, self
.block_text
),
449 return messages
+ [error
]
450 roles
._roles
[''] = role
451 # @@@ should this be local to the document, not the parser?
455 class Title(Directive
):
457 required_arguments
= 1
458 optional_arguments
= 0
459 final_argument_whitespace
= True
462 self
.state_machine
.document
['title'] = self
.arguments
[0]
466 class Date(Directive
):
471 if not isinstance(self
.state
, states
.SubstitutionDef
):
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
}
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
)
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
),
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."""
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)
515 # info = state_machine.reporter.info(
516 # 'Directive processed. Type="%s", arguments=%r, options=%r, '
517 # 'content: None' % (name, arguments, options), line=lineno)
520 # directive_test_function.arguments = (0, 1, 1)
521 # directive_test_function.options = {'option': directives.unchanged_required}
522 # directive_test_function.content = 1