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