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
.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 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)
82 if startline
or (endline
is not None):
83 lines
= include_file
.readlines()
84 rawtext
= ''.join(lines
[startline
:endline
])
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)
94 # skip content in rawtext before *and incl.* a matching text
95 after_index
= rawtext
.find(after_text
)
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)
102 # skip content in rawtext after *and incl.* a matching text
103 before_index
= rawtext
.find(before_text
)
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
=True)
111 if 'literal' in self
.options
:
112 # Convert tabs to spaces, if `tab_width` is positive.
114 text
= rawtext
.expandtabs(tab_width
)
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
:
123 startline
= int(self
.options
['number-lines'] or 1)
125 raise self
.error(':number-lines: with non-integer '
127 endline
= startline
+ len(include_lines
)
128 if text
.endswith('\n'):
130 tokens
= NumberLines([([], text
)], startline
, endline
)
131 for classes
, value
in tokens
:
133 literal_block
+= nodes
.inline(value
, value
,
136 literal_block
+= nodes
.Text(value
, value
)
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
145 include_lines
, # content
151 return codeblock
.run()
152 self
.state_machine
.insert_input(include_lines
, path
)
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
}
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
)
185 if 'file' in self
.options
or 'url' in self
.options
:
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
:
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
)
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
)))
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.
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
))
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
240 # This will always fail because there is no content.
241 self
.assert_has_content()
242 raw_node
= nodes
.raw('', text
, **attributes
)
244 raw_node
.line
) = self
.state_machine
.get_source_and_line(self
.lineno
)
248 class Replace(Directive
):
253 if not isinstance(self
.state
, states
.SubstitutionDef
):
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
,
262 # element might contain [paragraph] + system_message(s)
266 if not node
and isinstance(elem
, nodes
.paragraph
):
268 elif isinstance(elem
, nodes
.system_message
):
269 elem
['backrefs'] = []
270 messages
.append(elem
)
273 self
.state_machine
.reporter
.error(
274 'Error in "%s" directive: may contain a single paragraph '
275 'only.' % (self
.name
), line
=self
.lineno
) ]
277 return messages
+ node
.children
280 class Unicode(Directive
):
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. ``☮``). 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|^)\.\. ')
300 if not isinstance(self
.state
, states
.SubstitutionDef
):
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()
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
339 class_value
= directives
.class_option(self
.arguments
[0])
342 'Invalid class attribute value for "%s" directive: "%s".'
343 % (self
.name
, self
.arguments
[0]))
346 container
= nodes
.Element()
347 self
.state
.nested_parse(self
.content
, self
.content_offset
,
349 for node
in container
:
350 node
['classes'].extend(class_value
)
351 node_list
.extend(container
.children
)
353 pending
= nodes
.pending(
355 {'class': class_value
, 'directive': self
.name
},
357 self
.state_machine
.document
.note_pending(pending
)
358 node_list
.append(pending
)
362 class Role(Directive
):
366 argument_pattern
= re
.compile(r
'(%s)\s*(\(\s*(%s)\s*\)\s*)?$'
367 % ((states
.Inliner
.simplename
,) * 2))
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 '
374 args
= self
.content
[0]
375 match
= self
.argument_pattern
.match(args
)
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)
383 base_role
, messages
= roles
.role(
384 base_role_name
, self
.state_machine
.language
, self
.lineno
,
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
),
391 return messages
+ [error
]
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
))
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
,
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
),
408 return messages
+ [error
]
409 if 'class' not in options
:
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
)
423 class DefaultRole(Directive
):
425 """Set the default interpreted text role."""
427 optional_arguments
= 1
428 final_argument_whitespace
= False
431 if not self
.arguments
:
432 if '' in roles
._roles
:
433 # restore the "default" default role
436 role_name
= self
.arguments
[0]
437 role
, messages
= roles
.role(role_name
, self
.state_machine
.language
,
438 self
.lineno
, self
.state
.reporter
)
440 error
= self
.state
.reporter
.error(
441 'Unknown interpreted text role "%s".' % role_name
,
442 nodes
.literal_block(self
.block_text
, self
.block_text
),
444 return messages
+ [error
]
445 roles
._roles
[''] = role
446 # @@@ should this be local to the document, not the parser?
450 class Title(Directive
):
452 required_arguments
= 1
453 optional_arguments
= 0
454 final_argument_whitespace
= True
457 self
.state_machine
.document
['title'] = self
.arguments
[0]
461 class Date(Directive
):
466 if not isinstance(self
.state
, states
.SubstitutionDef
):
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
}
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
)
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
),
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."""
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)
510 # info = state_machine.reporter.info(
511 # 'Directive processed. Type="%s", arguments=%r, options=%r, '
512 # 'content: None' % (name, arguments, options), line=lineno)
515 # directive_test_function.arguments = (0, 1, 1)
516 # directive_test_function.options = {'option': directives.unchanged_required}
517 # directive_test_function.content = 1