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
.transforms
import misc
19 class Include(Directive
):
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
31 required_arguments
= 1
32 optional_arguments
= 0
33 final_argument_whitespace
= True
34 option_spec
= {'literal': directives
.flag
,
35 'encoding': directives
.encoding
,
39 'start-after': directives
.unchanged_required
,
40 'end-before': directives
.unchanged_required
}
42 standard_include_path
= os
.path
.join(os
.path
.dirname(states
.__file
__),
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
)
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)
75 if startline
or (endline
is not None):
76 lines
= include_file
.readlines()
77 rawtext
= ''.join(lines
[startline
:endline
])
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)
87 # skip content in rawtext before *and incl.* a matching text
88 after_index
= rawtext
.find(after_text
)
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)
95 # skip content in rawtext after *and incl.* a matching text
96 before_index
= rawtext
.find(before_text
)
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.
104 text
= rawtext
.expandtabs(tab_width
)
107 literal_block
= nodes
.literal_block(rawtext
, text
, source
=path
)
108 literal_block
.line
= 1
109 return [literal_block
]
111 include_lines
= statemachine
.string2lines(
112 rawtext
, tab_width
, convert_whitespace
=1)
113 self
.state_machine
.insert_input(include_lines
, path
)
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
}
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
)
146 if 'file' in self
.options
or 'url' in self
.options
:
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
:
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
)
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
)))
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.
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
))
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
199 # This will always fail because there is no content.
200 self
.assert_has_content()
201 raw_node
= nodes
.raw('', text
, **attributes
)
205 class Replace(Directive
):
210 if not isinstance(self
.state
, states
.SubstitutionDef
):
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
,
219 # element might contain [paragraph] + system_message(s)
223 if not node
and isinstance(elem
, nodes
.paragraph
):
225 elif isinstance(elem
, nodes
.system_message
):
226 elem
['backrefs'] = []
227 messages
.append(elem
)
230 self
.state_machine
.reporter
.error(
231 'Error in "%s" directive: may contain a single paragraph '
232 'only.' % (self
.name
), line
=self
.lineno
) ]
234 return messages
+ node
.children
237 class Unicode(Directive
):
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. ``☮``). 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|^)\.\. ')
257 if not isinstance(self
.state
, states
.SubstitutionDef
):
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()
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
296 class_value
= directives
.class_option(self
.arguments
[0])
299 'Invalid class attribute value for "%s" directive: "%s".'
300 % (self
.name
, self
.arguments
[0]))
303 container
= nodes
.Element()
304 self
.state
.nested_parse(self
.content
, self
.content_offset
,
306 for node
in container
:
307 node
['classes'].extend(class_value
)
308 node_list
.extend(container
.children
)
310 pending
= nodes
.pending(
312 {'class': class_value
, 'directive': self
.name
},
314 self
.state_machine
.document
.note_pending(pending
)
315 node_list
.append(pending
)
319 class Role(Directive
):
323 argument_pattern
= re
.compile(r
'(%s)\s*(\(\s*(%s)\s*\)\s*)?$'
324 % ((states
.Inliner
.simplename
,) * 2))
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 '
331 args
= self
.content
[0]
332 match
= self
.argument_pattern
.match(args
)
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)
340 base_role
, messages
= roles
.role(
341 base_role_name
, self
.state_machine
.language
, self
.lineno
,
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
),
348 return messages
+ [error
]
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
))
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
,
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
),
365 return messages
+ [error
]
366 if 'class' not in options
:
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
)
380 class DefaultRole(Directive
):
382 """Set the default interpreted text role."""
384 optional_arguments
= 1
385 final_argument_whitespace
= False
388 if not self
.arguments
:
389 if '' in roles
._roles
:
390 # restore the "default" default role
393 role_name
= self
.arguments
[0]
394 role
, messages
= roles
.role(role_name
, self
.state_machine
.language
,
395 self
.lineno
, self
.state
.reporter
)
397 error
= self
.state
.reporter
.error(
398 'Unknown interpreted text role "%s".' % role_name
,
399 nodes
.literal_block(self
.block_text
, self
.block_text
),
401 return messages
+ [error
]
402 roles
._roles
[''] = role
403 # @@@ should this be local to the document, not the parser?
407 class Title(Directive
):
409 required_arguments
= 1
410 optional_arguments
= 0
411 final_argument_whitespace
= True
414 self
.state_machine
.document
['title'] = self
.arguments
[0]
418 class Date(Directive
):
423 if not isinstance(self
.state
, states
.SubstitutionDef
):
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
}
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
)
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
),
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."""
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)
467 # info = state_machine.reporter.info(
468 # 'Directive processed. Type="%s", arguments=%r, options=%r, '
469 # 'content: None' % (name, arguments, options), line=lineno)
472 # directive_test_function.arguments = (0, 1, 1)
473 # directive_test_function.options = {'option': directives.unchanged_required}
474 # directive_test_function.content = 1