|
1 # $Id: __init__.py 5314 2007-07-05 12:45:43Z wiemann $ |
|
2 # Author: David Goodger <goodger@python.org> |
|
3 # Copyright: This module has been placed in the public domain. |
|
4 |
|
5 """ |
|
6 Simple HyperText Markup Language document tree Writer. |
|
7 |
|
8 The output conforms to the XHTML version 1.0 Transitional DTD |
|
9 (*almost* strict). The output contains a minimum of formatting |
|
10 information. The cascading style sheet "html4css1.css" is required |
|
11 for proper viewing with a modern graphical browser. |
|
12 """ |
|
13 |
|
14 __docformat__ = 'reStructuredText' |
|
15 |
|
16 |
|
17 import sys |
|
18 import os |
|
19 import os.path |
|
20 import time |
|
21 import re |
|
22 from types import ListType |
|
23 try: |
|
24 import Image # check for the Python Imaging Library |
|
25 except ImportError: |
|
26 Image = None |
|
27 import docutils |
|
28 from docutils import frontend, nodes, utils, writers, languages |
|
29 from docutils.transforms import writer_aux |
|
30 |
|
31 |
|
32 class Writer(writers.Writer): |
|
33 |
|
34 supported = ('html', 'html4css1', 'xhtml') |
|
35 """Formats this writer supports.""" |
|
36 |
|
37 default_stylesheet = 'html4css1.css' |
|
38 |
|
39 default_stylesheet_path = utils.relative_path( |
|
40 os.path.join(os.getcwd(), 'dummy'), |
|
41 os.path.join(os.path.dirname(__file__), default_stylesheet)) |
|
42 |
|
43 default_template = 'template.txt' |
|
44 |
|
45 default_template_path = utils.relative_path( |
|
46 os.path.join(os.getcwd(), 'dummy'), |
|
47 os.path.join(os.path.dirname(__file__), default_template)) |
|
48 |
|
49 settings_spec = ( |
|
50 'HTML-Specific Options', |
|
51 None, |
|
52 (('Specify the template file (UTF-8 encoded). Default is "%s".' |
|
53 % default_template_path, |
|
54 ['--template'], |
|
55 {'default': default_template_path, 'metavar': '<file>'}), |
|
56 ('Specify a stylesheet URL, used verbatim. Overrides ' |
|
57 '--stylesheet-path.', |
|
58 ['--stylesheet'], |
|
59 {'metavar': '<URL>', 'overrides': 'stylesheet_path'}), |
|
60 ('Specify a stylesheet file, relative to the current working ' |
|
61 'directory. The path is adjusted relative to the output HTML ' |
|
62 'file. Overrides --stylesheet. Default: "%s"' |
|
63 % default_stylesheet_path, |
|
64 ['--stylesheet-path'], |
|
65 {'metavar': '<file>', 'overrides': 'stylesheet', |
|
66 'default': default_stylesheet_path}), |
|
67 ('Embed the stylesheet in the output HTML file. The stylesheet ' |
|
68 'file must be accessible during processing (--stylesheet-path is ' |
|
69 'recommended). This is the default.', |
|
70 ['--embed-stylesheet'], |
|
71 {'default': 1, 'action': 'store_true', |
|
72 'validator': frontend.validate_boolean}), |
|
73 ('Link to the stylesheet in the output HTML file. Default: ' |
|
74 'embed the stylesheet, do not link to it.', |
|
75 ['--link-stylesheet'], |
|
76 {'dest': 'embed_stylesheet', 'action': 'store_false'}), |
|
77 ('Specify the initial header level. Default is 1 for "<h1>". ' |
|
78 'Does not affect document title & subtitle (see --no-doc-title).', |
|
79 ['--initial-header-level'], |
|
80 {'choices': '1 2 3 4 5 6'.split(), 'default': '1', |
|
81 'metavar': '<level>'}), |
|
82 ('Specify the maximum width (in characters) for one-column field ' |
|
83 'names. Longer field names will span an entire row of the table ' |
|
84 'used to render the field list. Default is 14 characters. ' |
|
85 'Use 0 for "no limit".', |
|
86 ['--field-name-limit'], |
|
87 {'default': 14, 'metavar': '<level>', |
|
88 'validator': frontend.validate_nonnegative_int}), |
|
89 ('Specify the maximum width (in characters) for options in option ' |
|
90 'lists. Longer options will span an entire row of the table used ' |
|
91 'to render the option list. Default is 14 characters. ' |
|
92 'Use 0 for "no limit".', |
|
93 ['--option-limit'], |
|
94 {'default': 14, 'metavar': '<level>', |
|
95 'validator': frontend.validate_nonnegative_int}), |
|
96 ('Format for footnote references: one of "superscript" or ' |
|
97 '"brackets". Default is "brackets".', |
|
98 ['--footnote-references'], |
|
99 {'choices': ['superscript', 'brackets'], 'default': 'brackets', |
|
100 'metavar': '<format>', |
|
101 'overrides': 'trim_footnote_reference_space'}), |
|
102 ('Format for block quote attributions: one of "dash" (em-dash ' |
|
103 'prefix), "parentheses"/"parens", or "none". Default is "dash".', |
|
104 ['--attribution'], |
|
105 {'choices': ['dash', 'parentheses', 'parens', 'none'], |
|
106 'default': 'dash', 'metavar': '<format>'}), |
|
107 ('Remove extra vertical whitespace between items of "simple" bullet ' |
|
108 'lists and enumerated lists. Default: enabled.', |
|
109 ['--compact-lists'], |
|
110 {'default': 1, 'action': 'store_true', |
|
111 'validator': frontend.validate_boolean}), |
|
112 ('Disable compact simple bullet and enumerated lists.', |
|
113 ['--no-compact-lists'], |
|
114 {'dest': 'compact_lists', 'action': 'store_false'}), |
|
115 ('Remove extra vertical whitespace between items of simple field ' |
|
116 'lists. Default: enabled.', |
|
117 ['--compact-field-lists'], |
|
118 {'default': 1, 'action': 'store_true', |
|
119 'validator': frontend.validate_boolean}), |
|
120 ('Disable compact simple field lists.', |
|
121 ['--no-compact-field-lists'], |
|
122 {'dest': 'compact_field_lists', 'action': 'store_false'}), |
|
123 ('Omit the XML declaration. Use with caution.', |
|
124 ['--no-xml-declaration'], |
|
125 {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false', |
|
126 'validator': frontend.validate_boolean}), |
|
127 ('Obfuscate email addresses to confuse harvesters while still ' |
|
128 'keeping email links usable with standards-compliant browsers.', |
|
129 ['--cloak-email-addresses'], |
|
130 {'action': 'store_true', 'validator': frontend.validate_boolean}),)) |
|
131 |
|
132 settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'} |
|
133 |
|
134 relative_path_settings = ('stylesheet_path',) |
|
135 |
|
136 config_section = 'html4css1 writer' |
|
137 config_section_dependencies = ('writers',) |
|
138 |
|
139 visitor_attributes = ( |
|
140 'head_prefix', 'head', 'stylesheet', 'body_prefix', |
|
141 'body_pre_docinfo', 'docinfo', 'body', 'body_suffix', |
|
142 'title', 'subtitle', 'header', 'footer', 'meta', 'fragment', |
|
143 'html_prolog', 'html_head', 'html_title', 'html_subtitle', |
|
144 'html_body') |
|
145 |
|
146 def get_transforms(self): |
|
147 return writers.Writer.get_transforms(self) + [writer_aux.Admonitions] |
|
148 |
|
149 def __init__(self): |
|
150 writers.Writer.__init__(self) |
|
151 self.translator_class = HTMLTranslator |
|
152 |
|
153 def translate(self): |
|
154 self.visitor = visitor = self.translator_class(self.document) |
|
155 self.document.walkabout(visitor) |
|
156 for attr in self.visitor_attributes: |
|
157 setattr(self, attr, getattr(visitor, attr)) |
|
158 self.output = self.apply_template() |
|
159 |
|
160 def apply_template(self): |
|
161 template_file = open(self.document.settings.template) |
|
162 template = unicode(template_file.read(), 'utf-8') |
|
163 template_file.close() |
|
164 subs = self.interpolation_dict() |
|
165 return template % subs |
|
166 |
|
167 def interpolation_dict(self): |
|
168 subs = {} |
|
169 settings = self.document.settings |
|
170 for attr in self.visitor_attributes: |
|
171 subs[attr] = ''.join(getattr(self, attr)).rstrip('\n') |
|
172 subs['encoding'] = settings.output_encoding |
|
173 subs['version'] = docutils.__version__ |
|
174 return subs |
|
175 |
|
176 def assemble_parts(self): |
|
177 writers.Writer.assemble_parts(self) |
|
178 for part in self.visitor_attributes: |
|
179 self.parts[part] = ''.join(getattr(self, part)) |
|
180 |
|
181 |
|
182 class HTMLTranslator(nodes.NodeVisitor): |
|
183 |
|
184 """ |
|
185 This HTML writer has been optimized to produce visually compact |
|
186 lists (less vertical whitespace). HTML's mixed content models |
|
187 allow list items to contain "<li><p>body elements</p></li>" or |
|
188 "<li>just text</li>" or even "<li>text<p>and body |
|
189 elements</p>combined</li>", each with different effects. It would |
|
190 be best to stick with strict body elements in list items, but they |
|
191 affect vertical spacing in browsers (although they really |
|
192 shouldn't). |
|
193 |
|
194 Here is an outline of the optimization: |
|
195 |
|
196 - Check for and omit <p> tags in "simple" lists: list items |
|
197 contain either a single paragraph, a nested simple list, or a |
|
198 paragraph followed by a nested simple list. This means that |
|
199 this list can be compact: |
|
200 |
|
201 - Item 1. |
|
202 - Item 2. |
|
203 |
|
204 But this list cannot be compact: |
|
205 |
|
206 - Item 1. |
|
207 |
|
208 This second paragraph forces space between list items. |
|
209 |
|
210 - Item 2. |
|
211 |
|
212 - In non-list contexts, omit <p> tags on a paragraph if that |
|
213 paragraph is the only child of its parent (footnotes & citations |
|
214 are allowed a label first). |
|
215 |
|
216 - Regardless of the above, in definitions, table cells, field bodies, |
|
217 option descriptions, and list items, mark the first child with |
|
218 'class="first"' and the last child with 'class="last"'. The stylesheet |
|
219 sets the margins (top & bottom respectively) to 0 for these elements. |
|
220 |
|
221 The ``no_compact_lists`` setting (``--no-compact-lists`` command-line |
|
222 option) disables list whitespace optimization. |
|
223 """ |
|
224 |
|
225 xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n' |
|
226 doctype = ( |
|
227 '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"' |
|
228 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n') |
|
229 head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"' |
|
230 ' xml:lang="%s" lang="%s">\n<head>\n') |
|
231 content_type = ('<meta http-equiv="Content-Type"' |
|
232 ' content="text/html; charset=%s" />\n') |
|
233 generator = ('<meta name="generator" content="Docutils %s: ' |
|
234 'http://docutils.sourceforge.net/" />\n') |
|
235 stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n' |
|
236 embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n' |
|
237 words_and_spaces = re.compile(r'\S+| +|\n') |
|
238 |
|
239 def __init__(self, document): |
|
240 nodes.NodeVisitor.__init__(self, document) |
|
241 self.settings = settings = document.settings |
|
242 lcode = settings.language_code |
|
243 self.language = languages.get_language(lcode) |
|
244 self.meta = [self.content_type % settings.output_encoding, |
|
245 self.generator % docutils.__version__] |
|
246 self.head_prefix = [] |
|
247 self.html_prolog = [] |
|
248 if settings.xml_declaration: |
|
249 self.head_prefix.append(self.xml_declaration |
|
250 % settings.output_encoding) |
|
251 # encoding not interpolated: |
|
252 self.html_prolog.append(self.xml_declaration) |
|
253 self.head_prefix.extend([self.doctype, |
|
254 self.head_prefix_template % (lcode, lcode)]) |
|
255 self.html_prolog.append(self.doctype) |
|
256 self.head = self.meta[:] |
|
257 stylesheet = utils.get_stylesheet_reference(settings) |
|
258 self.stylesheet = [] |
|
259 if stylesheet: |
|
260 if settings.embed_stylesheet: |
|
261 stylesheet = utils.get_stylesheet_reference( |
|
262 settings, os.path.join(os.getcwd(), 'dummy')) |
|
263 settings.record_dependencies.add(stylesheet) |
|
264 stylesheet_text = open(stylesheet).read() |
|
265 self.stylesheet = [self.embedded_stylesheet % stylesheet_text] |
|
266 else: |
|
267 self.stylesheet = [self.stylesheet_link |
|
268 % self.encode(stylesheet)] |
|
269 self.body_prefix = ['</head>\n<body>\n'] |
|
270 # document title, subtitle display |
|
271 self.body_pre_docinfo = [] |
|
272 # author, date, etc. |
|
273 self.docinfo = [] |
|
274 self.body = [] |
|
275 self.fragment = [] |
|
276 self.body_suffix = ['</body>\n</html>\n'] |
|
277 self.section_level = 0 |
|
278 self.initial_header_level = int(settings.initial_header_level) |
|
279 # A heterogenous stack used in conjunction with the tree traversal. |
|
280 # Make sure that the pops correspond to the pushes: |
|
281 self.context = [] |
|
282 self.topic_classes = [] |
|
283 self.colspecs = [] |
|
284 self.compact_p = 1 |
|
285 self.compact_simple = None |
|
286 self.compact_field_list = None |
|
287 self.in_docinfo = None |
|
288 self.in_sidebar = None |
|
289 self.title = [] |
|
290 self.subtitle = [] |
|
291 self.header = [] |
|
292 self.footer = [] |
|
293 self.html_head = [self.content_type] # charset not interpolated |
|
294 self.html_title = [] |
|
295 self.html_subtitle = [] |
|
296 self.html_body = [] |
|
297 self.in_document_title = 0 |
|
298 self.in_mailto = 0 |
|
299 self.author_in_authors = None |
|
300 |
|
301 def astext(self): |
|
302 return ''.join(self.head_prefix + self.head |
|
303 + self.stylesheet + self.body_prefix |
|
304 + self.body_pre_docinfo + self.docinfo |
|
305 + self.body + self.body_suffix) |
|
306 |
|
307 def encode(self, text): |
|
308 """Encode special characters in `text` & return.""" |
|
309 # @@@ A codec to do these and all other HTML entities would be nice. |
|
310 text = text.replace("&", "&") |
|
311 text = text.replace("<", "<") |
|
312 text = text.replace('"', """) |
|
313 text = text.replace(">", ">") |
|
314 text = text.replace("@", "@") # may thwart some address harvesters |
|
315 # Replace the non-breaking space character with the HTML entity: |
|
316 text = text.replace(u'\u00a0', " ") |
|
317 return text |
|
318 |
|
319 def cloak_mailto(self, uri): |
|
320 """Try to hide a mailto: URL from harvesters.""" |
|
321 # Encode "@" using a URL octet reference (see RFC 1738). |
|
322 # Further cloaking with HTML entities will be done in the |
|
323 # `attval` function. |
|
324 return uri.replace('@', '%40') |
|
325 |
|
326 def cloak_email(self, addr): |
|
327 """Try to hide the link text of a email link from harversters.""" |
|
328 # Surround at-signs and periods with <span> tags. ("@" has |
|
329 # already been encoded to "@" by the `encode` method.) |
|
330 addr = addr.replace('@', '<span>@</span>') |
|
331 addr = addr.replace('.', '<span>.</span>') |
|
332 return addr |
|
333 |
|
334 def attval(self, text, |
|
335 whitespace=re.compile('[\n\r\t\v\f]')): |
|
336 """Cleanse, HTML encode, and return attribute value text.""" |
|
337 encoded = self.encode(whitespace.sub(' ', text)) |
|
338 if self.in_mailto and self.settings.cloak_email_addresses: |
|
339 # Cloak at-signs ("%40") and periods with HTML entities. |
|
340 encoded = encoded.replace('%40', '%40') |
|
341 encoded = encoded.replace('.', '.') |
|
342 return encoded |
|
343 |
|
344 def starttag(self, node, tagname, suffix='\n', empty=0, **attributes): |
|
345 """ |
|
346 Construct and return a start tag given a node (id & class attributes |
|
347 are extracted), tag name, and optional attributes. |
|
348 """ |
|
349 tagname = tagname.lower() |
|
350 prefix = [] |
|
351 atts = {} |
|
352 ids = [] |
|
353 for (name, value) in attributes.items(): |
|
354 atts[name.lower()] = value |
|
355 classes = node.get('classes', []) |
|
356 if atts.has_key('class'): |
|
357 classes.append(atts['class']) |
|
358 if classes: |
|
359 atts['class'] = ' '.join(classes) |
|
360 assert not atts.has_key('id') |
|
361 ids.extend(node.get('ids', [])) |
|
362 if atts.has_key('ids'): |
|
363 ids.extend(atts['ids']) |
|
364 del atts['ids'] |
|
365 if ids: |
|
366 atts['id'] = ids[0] |
|
367 for id in ids[1:]: |
|
368 # Add empty "span" elements for additional IDs. Note |
|
369 # that we cannot use empty "a" elements because there |
|
370 # may be targets inside of references, but nested "a" |
|
371 # elements aren't allowed in XHTML (even if they do |
|
372 # not all have a "href" attribute). |
|
373 if empty: |
|
374 # Empty tag. Insert target right in front of element. |
|
375 prefix.append('<span id="%s"></span>' % id) |
|
376 else: |
|
377 # Non-empty tag. Place the auxiliary <span> tag |
|
378 # *inside* the element, as the first child. |
|
379 suffix += '<span id="%s"></span>' % id |
|
380 attlist = atts.items() |
|
381 attlist.sort() |
|
382 parts = [tagname] |
|
383 for name, value in attlist: |
|
384 # value=None was used for boolean attributes without |
|
385 # value, but this isn't supported by XHTML. |
|
386 assert value is not None |
|
387 if isinstance(value, ListType): |
|
388 values = [unicode(v) for v in value] |
|
389 parts.append('%s="%s"' % (name.lower(), |
|
390 self.attval(' '.join(values)))) |
|
391 else: |
|
392 parts.append('%s="%s"' % (name.lower(), |
|
393 self.attval(unicode(value)))) |
|
394 if empty: |
|
395 infix = ' /' |
|
396 else: |
|
397 infix = '' |
|
398 return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix |
|
399 |
|
400 def emptytag(self, node, tagname, suffix='\n', **attributes): |
|
401 """Construct and return an XML-compatible empty tag.""" |
|
402 return self.starttag(node, tagname, suffix, empty=1, **attributes) |
|
403 |
|
404 def set_class_on_child(self, node, class_, index=0): |
|
405 """ |
|
406 Set class `class_` on the visible child no. index of `node`. |
|
407 Do nothing if node has fewer children than `index`. |
|
408 """ |
|
409 children = [n for n in node if not isinstance(n, nodes.Invisible)] |
|
410 try: |
|
411 child = children[index] |
|
412 except IndexError: |
|
413 return |
|
414 child['classes'].append(class_) |
|
415 |
|
416 def set_first_last(self, node): |
|
417 self.set_class_on_child(node, 'first', 0) |
|
418 self.set_class_on_child(node, 'last', -1) |
|
419 |
|
420 def visit_Text(self, node): |
|
421 text = node.astext() |
|
422 encoded = self.encode(text) |
|
423 if self.in_mailto and self.settings.cloak_email_addresses: |
|
424 encoded = self.cloak_email(encoded) |
|
425 self.body.append(encoded) |
|
426 |
|
427 def depart_Text(self, node): |
|
428 pass |
|
429 |
|
430 def visit_abbreviation(self, node): |
|
431 # @@@ implementation incomplete ("title" attribute) |
|
432 self.body.append(self.starttag(node, 'abbr', '')) |
|
433 |
|
434 def depart_abbreviation(self, node): |
|
435 self.body.append('</abbr>') |
|
436 |
|
437 def visit_acronym(self, node): |
|
438 # @@@ implementation incomplete ("title" attribute) |
|
439 self.body.append(self.starttag(node, 'acronym', '')) |
|
440 |
|
441 def depart_acronym(self, node): |
|
442 self.body.append('</acronym>') |
|
443 |
|
444 def visit_address(self, node): |
|
445 self.visit_docinfo_item(node, 'address', meta=None) |
|
446 self.body.append(self.starttag(node, 'pre', CLASS='address')) |
|
447 |
|
448 def depart_address(self, node): |
|
449 self.body.append('\n</pre>\n') |
|
450 self.depart_docinfo_item() |
|
451 |
|
452 def visit_admonition(self, node): |
|
453 self.body.append(self.starttag(node, 'div')) |
|
454 self.set_first_last(node) |
|
455 |
|
456 def depart_admonition(self, node=None): |
|
457 self.body.append('</div>\n') |
|
458 |
|
459 attribution_formats = {'dash': ('—', ''), |
|
460 'parentheses': ('(', ')'), |
|
461 'parens': ('(', ')'), |
|
462 'none': ('', '')} |
|
463 |
|
464 def visit_attribution(self, node): |
|
465 prefix, suffix = self.attribution_formats[self.settings.attribution] |
|
466 self.context.append(suffix) |
|
467 self.body.append( |
|
468 self.starttag(node, 'p', prefix, CLASS='attribution')) |
|
469 |
|
470 def depart_attribution(self, node): |
|
471 self.body.append(self.context.pop() + '</p>\n') |
|
472 |
|
473 def visit_author(self, node): |
|
474 if isinstance(node.parent, nodes.authors): |
|
475 if self.author_in_authors: |
|
476 self.body.append('\n<br />') |
|
477 else: |
|
478 self.visit_docinfo_item(node, 'author') |
|
479 |
|
480 def depart_author(self, node): |
|
481 if isinstance(node.parent, nodes.authors): |
|
482 self.author_in_authors += 1 |
|
483 else: |
|
484 self.depart_docinfo_item() |
|
485 |
|
486 def visit_authors(self, node): |
|
487 self.visit_docinfo_item(node, 'authors') |
|
488 self.author_in_authors = 0 # initialize counter |
|
489 |
|
490 def depart_authors(self, node): |
|
491 self.depart_docinfo_item() |
|
492 self.author_in_authors = None |
|
493 |
|
494 def visit_block_quote(self, node): |
|
495 self.body.append(self.starttag(node, 'blockquote')) |
|
496 |
|
497 def depart_block_quote(self, node): |
|
498 self.body.append('</blockquote>\n') |
|
499 |
|
500 def check_simple_list(self, node): |
|
501 """Check for a simple list that can be rendered compactly.""" |
|
502 visitor = SimpleListChecker(self.document) |
|
503 try: |
|
504 node.walk(visitor) |
|
505 except nodes.NodeFound: |
|
506 return None |
|
507 else: |
|
508 return 1 |
|
509 |
|
510 def is_compactable(self, node): |
|
511 return ('compact' in node['classes'] |
|
512 or (self.settings.compact_lists |
|
513 and 'open' not in node['classes'] |
|
514 and (self.compact_simple |
|
515 or self.topic_classes == ['contents'] |
|
516 or self.check_simple_list(node)))) |
|
517 |
|
518 def visit_bullet_list(self, node): |
|
519 atts = {} |
|
520 old_compact_simple = self.compact_simple |
|
521 self.context.append((self.compact_simple, self.compact_p)) |
|
522 self.compact_p = None |
|
523 self.compact_simple = self.is_compactable(node) |
|
524 if self.compact_simple and not old_compact_simple: |
|
525 atts['class'] = 'simple' |
|
526 self.body.append(self.starttag(node, 'ul', **atts)) |
|
527 |
|
528 def depart_bullet_list(self, node): |
|
529 self.compact_simple, self.compact_p = self.context.pop() |
|
530 self.body.append('</ul>\n') |
|
531 |
|
532 def visit_caption(self, node): |
|
533 self.body.append(self.starttag(node, 'p', '', CLASS='caption')) |
|
534 |
|
535 def depart_caption(self, node): |
|
536 self.body.append('</p>\n') |
|
537 |
|
538 def visit_citation(self, node): |
|
539 self.body.append(self.starttag(node, 'table', |
|
540 CLASS='docutils citation', |
|
541 frame="void", rules="none")) |
|
542 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n' |
|
543 '<tbody valign="top">\n' |
|
544 '<tr>') |
|
545 self.footnote_backrefs(node) |
|
546 |
|
547 def depart_citation(self, node): |
|
548 self.body.append('</td></tr>\n' |
|
549 '</tbody>\n</table>\n') |
|
550 |
|
551 def visit_citation_reference(self, node): |
|
552 href = '#' + node['refid'] |
|
553 self.body.append(self.starttag( |
|
554 node, 'a', '[', CLASS='citation-reference', href=href)) |
|
555 |
|
556 def depart_citation_reference(self, node): |
|
557 self.body.append(']</a>') |
|
558 |
|
559 def visit_classifier(self, node): |
|
560 self.body.append(' <span class="classifier-delimiter">:</span> ') |
|
561 self.body.append(self.starttag(node, 'span', '', CLASS='classifier')) |
|
562 |
|
563 def depart_classifier(self, node): |
|
564 self.body.append('</span>') |
|
565 |
|
566 def visit_colspec(self, node): |
|
567 self.colspecs.append(node) |
|
568 # "stubs" list is an attribute of the tgroup element: |
|
569 node.parent.stubs.append(node.attributes.get('stub')) |
|
570 |
|
571 def depart_colspec(self, node): |
|
572 pass |
|
573 |
|
574 def write_colspecs(self): |
|
575 width = 0 |
|
576 for node in self.colspecs: |
|
577 width += node['colwidth'] |
|
578 for node in self.colspecs: |
|
579 colwidth = int(node['colwidth'] * 100.0 / width + 0.5) |
|
580 self.body.append(self.emptytag(node, 'col', |
|
581 width='%i%%' % colwidth)) |
|
582 self.colspecs = [] |
|
583 |
|
584 def visit_comment(self, node, |
|
585 sub=re.compile('-(?=-)').sub): |
|
586 """Escape double-dashes in comment text.""" |
|
587 self.body.append('<!-- %s -->\n' % sub('- ', node.astext())) |
|
588 # Content already processed: |
|
589 raise nodes.SkipNode |
|
590 |
|
591 def visit_compound(self, node): |
|
592 self.body.append(self.starttag(node, 'div', CLASS='compound')) |
|
593 if len(node) > 1: |
|
594 node[0]['classes'].append('compound-first') |
|
595 node[-1]['classes'].append('compound-last') |
|
596 for child in node[1:-1]: |
|
597 child['classes'].append('compound-middle') |
|
598 |
|
599 def depart_compound(self, node): |
|
600 self.body.append('</div>\n') |
|
601 |
|
602 def visit_container(self, node): |
|
603 self.body.append(self.starttag(node, 'div', CLASS='container')) |
|
604 |
|
605 def depart_container(self, node): |
|
606 self.body.append('</div>\n') |
|
607 |
|
608 def visit_contact(self, node): |
|
609 self.visit_docinfo_item(node, 'contact', meta=None) |
|
610 |
|
611 def depart_contact(self, node): |
|
612 self.depart_docinfo_item() |
|
613 |
|
614 def visit_copyright(self, node): |
|
615 self.visit_docinfo_item(node, 'copyright') |
|
616 |
|
617 def depart_copyright(self, node): |
|
618 self.depart_docinfo_item() |
|
619 |
|
620 def visit_date(self, node): |
|
621 self.visit_docinfo_item(node, 'date') |
|
622 |
|
623 def depart_date(self, node): |
|
624 self.depart_docinfo_item() |
|
625 |
|
626 def visit_decoration(self, node): |
|
627 pass |
|
628 |
|
629 def depart_decoration(self, node): |
|
630 pass |
|
631 |
|
632 def visit_definition(self, node): |
|
633 self.body.append('</dt>\n') |
|
634 self.body.append(self.starttag(node, 'dd', '')) |
|
635 self.set_first_last(node) |
|
636 |
|
637 def depart_definition(self, node): |
|
638 self.body.append('</dd>\n') |
|
639 |
|
640 def visit_definition_list(self, node): |
|
641 self.body.append(self.starttag(node, 'dl', CLASS='docutils')) |
|
642 |
|
643 def depart_definition_list(self, node): |
|
644 self.body.append('</dl>\n') |
|
645 |
|
646 def visit_definition_list_item(self, node): |
|
647 pass |
|
648 |
|
649 def depart_definition_list_item(self, node): |
|
650 pass |
|
651 |
|
652 def visit_description(self, node): |
|
653 self.body.append(self.starttag(node, 'td', '')) |
|
654 self.set_first_last(node) |
|
655 |
|
656 def depart_description(self, node): |
|
657 self.body.append('</td>') |
|
658 |
|
659 def visit_docinfo(self, node): |
|
660 self.context.append(len(self.body)) |
|
661 self.body.append(self.starttag(node, 'table', |
|
662 CLASS='docinfo', |
|
663 frame="void", rules="none")) |
|
664 self.body.append('<col class="docinfo-name" />\n' |
|
665 '<col class="docinfo-content" />\n' |
|
666 '<tbody valign="top">\n') |
|
667 self.in_docinfo = 1 |
|
668 |
|
669 def depart_docinfo(self, node): |
|
670 self.body.append('</tbody>\n</table>\n') |
|
671 self.in_docinfo = None |
|
672 start = self.context.pop() |
|
673 self.docinfo = self.body[start:] |
|
674 self.body = [] |
|
675 |
|
676 def visit_docinfo_item(self, node, name, meta=1): |
|
677 if meta: |
|
678 meta_tag = '<meta name="%s" content="%s" />\n' \ |
|
679 % (name, self.attval(node.astext())) |
|
680 self.add_meta(meta_tag) |
|
681 self.body.append(self.starttag(node, 'tr', '')) |
|
682 self.body.append('<th class="docinfo-name">%s:</th>\n<td>' |
|
683 % self.language.labels[name]) |
|
684 if len(node): |
|
685 if isinstance(node[0], nodes.Element): |
|
686 node[0]['classes'].append('first') |
|
687 if isinstance(node[-1], nodes.Element): |
|
688 node[-1]['classes'].append('last') |
|
689 |
|
690 def depart_docinfo_item(self): |
|
691 self.body.append('</td></tr>\n') |
|
692 |
|
693 def visit_doctest_block(self, node): |
|
694 self.body.append(self.starttag(node, 'pre', CLASS='doctest-block')) |
|
695 |
|
696 def depart_doctest_block(self, node): |
|
697 self.body.append('\n</pre>\n') |
|
698 |
|
699 def visit_document(self, node): |
|
700 self.head.append('<title>%s</title>\n' |
|
701 % self.encode(node.get('title', ''))) |
|
702 |
|
703 def depart_document(self, node): |
|
704 self.fragment.extend(self.body) |
|
705 self.body_prefix.append(self.starttag(node, 'div', CLASS='document')) |
|
706 self.body_suffix.insert(0, '</div>\n') |
|
707 # skip content-type meta tag with interpolated charset value: |
|
708 self.html_head.extend(self.head[1:]) |
|
709 self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo |
|
710 + self.docinfo + self.body |
|
711 + self.body_suffix[:-1]) |
|
712 assert not self.context, 'len(context) = %s' % len(self.context) |
|
713 |
|
714 def visit_emphasis(self, node): |
|
715 self.body.append('<em>') |
|
716 |
|
717 def depart_emphasis(self, node): |
|
718 self.body.append('</em>') |
|
719 |
|
720 def visit_entry(self, node): |
|
721 atts = {'class': []} |
|
722 if isinstance(node.parent.parent, nodes.thead): |
|
723 atts['class'].append('head') |
|
724 if node.parent.parent.parent.stubs[node.parent.column]: |
|
725 # "stubs" list is an attribute of the tgroup element |
|
726 atts['class'].append('stub') |
|
727 if atts['class']: |
|
728 tagname = 'th' |
|
729 atts['class'] = ' '.join(atts['class']) |
|
730 else: |
|
731 tagname = 'td' |
|
732 del atts['class'] |
|
733 node.parent.column += 1 |
|
734 if node.has_key('morerows'): |
|
735 atts['rowspan'] = node['morerows'] + 1 |
|
736 if node.has_key('morecols'): |
|
737 atts['colspan'] = node['morecols'] + 1 |
|
738 node.parent.column += node['morecols'] |
|
739 self.body.append(self.starttag(node, tagname, '', **atts)) |
|
740 self.context.append('</%s>\n' % tagname.lower()) |
|
741 if len(node) == 0: # empty cell |
|
742 self.body.append(' ') |
|
743 self.set_first_last(node) |
|
744 |
|
745 def depart_entry(self, node): |
|
746 self.body.append(self.context.pop()) |
|
747 |
|
748 def visit_enumerated_list(self, node): |
|
749 """ |
|
750 The 'start' attribute does not conform to HTML 4.01's strict.dtd, but |
|
751 CSS1 doesn't help. CSS2 isn't widely enough supported yet to be |
|
752 usable. |
|
753 """ |
|
754 atts = {} |
|
755 if node.has_key('start'): |
|
756 atts['start'] = node['start'] |
|
757 if node.has_key('enumtype'): |
|
758 atts['class'] = node['enumtype'] |
|
759 # @@@ To do: prefix, suffix. How? Change prefix/suffix to a |
|
760 # single "format" attribute? Use CSS2? |
|
761 old_compact_simple = self.compact_simple |
|
762 self.context.append((self.compact_simple, self.compact_p)) |
|
763 self.compact_p = None |
|
764 self.compact_simple = self.is_compactable(node) |
|
765 if self.compact_simple and not old_compact_simple: |
|
766 atts['class'] = (atts.get('class', '') + ' simple').strip() |
|
767 self.body.append(self.starttag(node, 'ol', **atts)) |
|
768 |
|
769 def depart_enumerated_list(self, node): |
|
770 self.compact_simple, self.compact_p = self.context.pop() |
|
771 self.body.append('</ol>\n') |
|
772 |
|
773 def visit_field(self, node): |
|
774 self.body.append(self.starttag(node, 'tr', '', CLASS='field')) |
|
775 |
|
776 def depart_field(self, node): |
|
777 self.body.append('</tr>\n') |
|
778 |
|
779 def visit_field_body(self, node): |
|
780 self.body.append(self.starttag(node, 'td', '', CLASS='field-body')) |
|
781 self.set_class_on_child(node, 'first', 0) |
|
782 field = node.parent |
|
783 if (self.compact_field_list or |
|
784 isinstance(field.parent, nodes.docinfo) or |
|
785 field.parent.index(field) == len(field.parent) - 1): |
|
786 # If we are in a compact list, the docinfo, or if this is |
|
787 # the last field of the field list, do not add vertical |
|
788 # space after last element. |
|
789 self.set_class_on_child(node, 'last', -1) |
|
790 |
|
791 def depart_field_body(self, node): |
|
792 self.body.append('</td>\n') |
|
793 |
|
794 def visit_field_list(self, node): |
|
795 self.context.append((self.compact_field_list, self.compact_p)) |
|
796 self.compact_p = None |
|
797 if 'compact' in node['classes']: |
|
798 self.compact_field_list = 1 |
|
799 elif (self.settings.compact_field_lists |
|
800 and 'open' not in node['classes']): |
|
801 self.compact_field_list = 1 |
|
802 if self.compact_field_list: |
|
803 for field in node: |
|
804 field_body = field[-1] |
|
805 assert isinstance(field_body, nodes.field_body) |
|
806 children = [n for n in field_body |
|
807 if not isinstance(n, nodes.Invisible)] |
|
808 if not (len(children) == 0 or |
|
809 len(children) == 1 and |
|
810 isinstance(children[0], nodes.paragraph)): |
|
811 self.compact_field_list = 0 |
|
812 break |
|
813 self.body.append(self.starttag(node, 'table', frame='void', |
|
814 rules='none', |
|
815 CLASS='docutils field-list')) |
|
816 self.body.append('<col class="field-name" />\n' |
|
817 '<col class="field-body" />\n' |
|
818 '<tbody valign="top">\n') |
|
819 |
|
820 def depart_field_list(self, node): |
|
821 self.body.append('</tbody>\n</table>\n') |
|
822 self.compact_field_list, self.compact_p = self.context.pop() |
|
823 |
|
824 def visit_field_name(self, node): |
|
825 atts = {} |
|
826 if self.in_docinfo: |
|
827 atts['class'] = 'docinfo-name' |
|
828 else: |
|
829 atts['class'] = 'field-name' |
|
830 if ( self.settings.field_name_limit |
|
831 and len(node.astext()) > self.settings.field_name_limit): |
|
832 atts['colspan'] = 2 |
|
833 self.context.append('</tr>\n<tr><td> </td>') |
|
834 else: |
|
835 self.context.append('') |
|
836 self.body.append(self.starttag(node, 'th', '', **atts)) |
|
837 |
|
838 def depart_field_name(self, node): |
|
839 self.body.append(':</th>') |
|
840 self.body.append(self.context.pop()) |
|
841 |
|
842 def visit_figure(self, node): |
|
843 atts = {'class': 'figure'} |
|
844 if node.get('width'): |
|
845 atts['style'] = 'width: %spx' % node['width'] |
|
846 if node.get('align'): |
|
847 atts['align'] = node['align'] |
|
848 self.body.append(self.starttag(node, 'div', **atts)) |
|
849 |
|
850 def depart_figure(self, node): |
|
851 self.body.append('</div>\n') |
|
852 |
|
853 def visit_footer(self, node): |
|
854 self.context.append(len(self.body)) |
|
855 |
|
856 def depart_footer(self, node): |
|
857 start = self.context.pop() |
|
858 footer = [self.starttag(node, 'div', CLASS='footer'), |
|
859 '<hr class="footer" />\n'] |
|
860 footer.extend(self.body[start:]) |
|
861 footer.append('\n</div>\n') |
|
862 self.footer.extend(footer) |
|
863 self.body_suffix[:0] = footer |
|
864 del self.body[start:] |
|
865 |
|
866 def visit_footnote(self, node): |
|
867 self.body.append(self.starttag(node, 'table', |
|
868 CLASS='docutils footnote', |
|
869 frame="void", rules="none")) |
|
870 self.body.append('<colgroup><col class="label" /><col /></colgroup>\n' |
|
871 '<tbody valign="top">\n' |
|
872 '<tr>') |
|
873 self.footnote_backrefs(node) |
|
874 |
|
875 def footnote_backrefs(self, node): |
|
876 backlinks = [] |
|
877 backrefs = node['backrefs'] |
|
878 if self.settings.footnote_backlinks and backrefs: |
|
879 if len(backrefs) == 1: |
|
880 self.context.append('') |
|
881 self.context.append('</a>') |
|
882 self.context.append('<a class="fn-backref" href="#%s">' |
|
883 % backrefs[0]) |
|
884 else: |
|
885 i = 1 |
|
886 for backref in backrefs: |
|
887 backlinks.append('<a class="fn-backref" href="#%s">%s</a>' |
|
888 % (backref, i)) |
|
889 i += 1 |
|
890 self.context.append('<em>(%s)</em> ' % ', '.join(backlinks)) |
|
891 self.context += ['', ''] |
|
892 else: |
|
893 self.context.append('') |
|
894 self.context += ['', ''] |
|
895 # If the node does not only consist of a label. |
|
896 if len(node) > 1: |
|
897 # If there are preceding backlinks, we do not set class |
|
898 # 'first', because we need to retain the top-margin. |
|
899 if not backlinks: |
|
900 node[1]['classes'].append('first') |
|
901 node[-1]['classes'].append('last') |
|
902 |
|
903 def depart_footnote(self, node): |
|
904 self.body.append('</td></tr>\n' |
|
905 '</tbody>\n</table>\n') |
|
906 |
|
907 def visit_footnote_reference(self, node): |
|
908 href = '#' + node['refid'] |
|
909 format = self.settings.footnote_references |
|
910 if format == 'brackets': |
|
911 suffix = '[' |
|
912 self.context.append(']') |
|
913 else: |
|
914 assert format == 'superscript' |
|
915 suffix = '<sup>' |
|
916 self.context.append('</sup>') |
|
917 self.body.append(self.starttag(node, 'a', suffix, |
|
918 CLASS='footnote-reference', href=href)) |
|
919 |
|
920 def depart_footnote_reference(self, node): |
|
921 self.body.append(self.context.pop() + '</a>') |
|
922 |
|
923 def visit_generated(self, node): |
|
924 pass |
|
925 |
|
926 def depart_generated(self, node): |
|
927 pass |
|
928 |
|
929 def visit_header(self, node): |
|
930 self.context.append(len(self.body)) |
|
931 |
|
932 def depart_header(self, node): |
|
933 start = self.context.pop() |
|
934 header = [self.starttag(node, 'div', CLASS='header')] |
|
935 header.extend(self.body[start:]) |
|
936 header.append('\n<hr class="header"/>\n</div>\n') |
|
937 self.body_prefix.extend(header) |
|
938 self.header.extend(header) |
|
939 del self.body[start:] |
|
940 |
|
941 def visit_image(self, node): |
|
942 atts = {} |
|
943 atts['src'] = node['uri'] |
|
944 if node.has_key('width'): |
|
945 atts['width'] = node['width'] |
|
946 if node.has_key('height'): |
|
947 atts['height'] = node['height'] |
|
948 if node.has_key('scale'): |
|
949 if Image and not (node.has_key('width') |
|
950 and node.has_key('height')): |
|
951 try: |
|
952 im = Image.open(str(atts['src'])) |
|
953 except (IOError, # Source image can't be found or opened |
|
954 UnicodeError): # PIL doesn't like Unicode paths. |
|
955 pass |
|
956 else: |
|
957 if not atts.has_key('width'): |
|
958 atts['width'] = str(im.size[0]) |
|
959 if not atts.has_key('height'): |
|
960 atts['height'] = str(im.size[1]) |
|
961 del im |
|
962 for att_name in 'width', 'height': |
|
963 if atts.has_key(att_name): |
|
964 match = re.match(r'([0-9.]+)(\S*)$', atts[att_name]) |
|
965 assert match |
|
966 atts[att_name] = '%s%s' % ( |
|
967 float(match.group(1)) * (float(node['scale']) / 100), |
|
968 match.group(2)) |
|
969 style = [] |
|
970 for att_name in 'width', 'height': |
|
971 if atts.has_key(att_name): |
|
972 if re.match(r'^[0-9.]+$', atts[att_name]): |
|
973 # Interpret unitless values as pixels. |
|
974 atts[att_name] += 'px' |
|
975 style.append('%s: %s;' % (att_name, atts[att_name])) |
|
976 del atts[att_name] |
|
977 if style: |
|
978 atts['style'] = ' '.join(style) |
|
979 atts['alt'] = node.get('alt', atts['src']) |
|
980 if (isinstance(node.parent, nodes.TextElement) or |
|
981 (isinstance(node.parent, nodes.reference) and |
|
982 not isinstance(node.parent.parent, nodes.TextElement))): |
|
983 # Inline context or surrounded by <a>...</a>. |
|
984 suffix = '' |
|
985 else: |
|
986 suffix = '\n' |
|
987 if node.has_key('align'): |
|
988 if node['align'] == 'center': |
|
989 # "align" attribute is set in surrounding "div" element. |
|
990 self.body.append('<div align="center" class="align-center">') |
|
991 self.context.append('</div>\n') |
|
992 suffix = '' |
|
993 else: |
|
994 # "align" attribute is set in "img" element. |
|
995 atts['align'] = node['align'] |
|
996 self.context.append('') |
|
997 atts['class'] = 'align-%s' % node['align'] |
|
998 else: |
|
999 self.context.append('') |
|
1000 self.body.append(self.emptytag(node, 'img', suffix, **atts)) |
|
1001 |
|
1002 def depart_image(self, node): |
|
1003 self.body.append(self.context.pop()) |
|
1004 |
|
1005 def visit_inline(self, node): |
|
1006 self.body.append(self.starttag(node, 'span', '')) |
|
1007 |
|
1008 def depart_inline(self, node): |
|
1009 self.body.append('</span>') |
|
1010 |
|
1011 def visit_label(self, node): |
|
1012 # Context added in footnote_backrefs. |
|
1013 self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(), |
|
1014 CLASS='label')) |
|
1015 |
|
1016 def depart_label(self, node): |
|
1017 # Context added in footnote_backrefs. |
|
1018 self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop())) |
|
1019 |
|
1020 def visit_legend(self, node): |
|
1021 self.body.append(self.starttag(node, 'div', CLASS='legend')) |
|
1022 |
|
1023 def depart_legend(self, node): |
|
1024 self.body.append('</div>\n') |
|
1025 |
|
1026 def visit_line(self, node): |
|
1027 self.body.append(self.starttag(node, 'div', suffix='', CLASS='line')) |
|
1028 if not len(node): |
|
1029 self.body.append('<br />') |
|
1030 |
|
1031 def depart_line(self, node): |
|
1032 self.body.append('</div>\n') |
|
1033 |
|
1034 def visit_line_block(self, node): |
|
1035 self.body.append(self.starttag(node, 'div', CLASS='line-block')) |
|
1036 |
|
1037 def depart_line_block(self, node): |
|
1038 self.body.append('</div>\n') |
|
1039 |
|
1040 def visit_list_item(self, node): |
|
1041 self.body.append(self.starttag(node, 'li', '')) |
|
1042 if len(node): |
|
1043 node[0]['classes'].append('first') |
|
1044 |
|
1045 def depart_list_item(self, node): |
|
1046 self.body.append('</li>\n') |
|
1047 |
|
1048 def visit_literal(self, node): |
|
1049 """Process text to prevent tokens from wrapping.""" |
|
1050 self.body.append( |
|
1051 self.starttag(node, 'tt', '', CLASS='docutils literal')) |
|
1052 text = node.astext() |
|
1053 for token in self.words_and_spaces.findall(text): |
|
1054 if token.strip(): |
|
1055 # Protect text like "--an-option" from bad line wrapping: |
|
1056 self.body.append('<span class="pre">%s</span>' |
|
1057 % self.encode(token)) |
|
1058 elif token in ('\n', ' '): |
|
1059 # Allow breaks at whitespace: |
|
1060 self.body.append(token) |
|
1061 else: |
|
1062 # Protect runs of multiple spaces; the last space can wrap: |
|
1063 self.body.append(' ' * (len(token) - 1) + ' ') |
|
1064 self.body.append('</tt>') |
|
1065 # Content already processed: |
|
1066 raise nodes.SkipNode |
|
1067 |
|
1068 def visit_literal_block(self, node): |
|
1069 self.body.append(self.starttag(node, 'pre', CLASS='literal-block')) |
|
1070 |
|
1071 def depart_literal_block(self, node): |
|
1072 self.body.append('\n</pre>\n') |
|
1073 |
|
1074 def visit_meta(self, node): |
|
1075 meta = self.emptytag(node, 'meta', **node.non_default_attributes()) |
|
1076 self.add_meta(meta) |
|
1077 |
|
1078 def depart_meta(self, node): |
|
1079 pass |
|
1080 |
|
1081 def add_meta(self, tag): |
|
1082 self.meta.append(tag) |
|
1083 self.head.append(tag) |
|
1084 |
|
1085 def visit_option(self, node): |
|
1086 if self.context[-1]: |
|
1087 self.body.append(', ') |
|
1088 self.body.append(self.starttag(node, 'span', '', CLASS='option')) |
|
1089 |
|
1090 def depart_option(self, node): |
|
1091 self.body.append('</span>') |
|
1092 self.context[-1] += 1 |
|
1093 |
|
1094 def visit_option_argument(self, node): |
|
1095 self.body.append(node.get('delimiter', ' ')) |
|
1096 self.body.append(self.starttag(node, 'var', '')) |
|
1097 |
|
1098 def depart_option_argument(self, node): |
|
1099 self.body.append('</var>') |
|
1100 |
|
1101 def visit_option_group(self, node): |
|
1102 atts = {} |
|
1103 if ( self.settings.option_limit |
|
1104 and len(node.astext()) > self.settings.option_limit): |
|
1105 atts['colspan'] = 2 |
|
1106 self.context.append('</tr>\n<tr><td> </td>') |
|
1107 else: |
|
1108 self.context.append('') |
|
1109 self.body.append( |
|
1110 self.starttag(node, 'td', CLASS='option-group', **atts)) |
|
1111 self.body.append('<kbd>') |
|
1112 self.context.append(0) # count number of options |
|
1113 |
|
1114 def depart_option_group(self, node): |
|
1115 self.context.pop() |
|
1116 self.body.append('</kbd></td>\n') |
|
1117 self.body.append(self.context.pop()) |
|
1118 |
|
1119 def visit_option_list(self, node): |
|
1120 self.body.append( |
|
1121 self.starttag(node, 'table', CLASS='docutils option-list', |
|
1122 frame="void", rules="none")) |
|
1123 self.body.append('<col class="option" />\n' |
|
1124 '<col class="description" />\n' |
|
1125 '<tbody valign="top">\n') |
|
1126 |
|
1127 def depart_option_list(self, node): |
|
1128 self.body.append('</tbody>\n</table>\n') |
|
1129 |
|
1130 def visit_option_list_item(self, node): |
|
1131 self.body.append(self.starttag(node, 'tr', '')) |
|
1132 |
|
1133 def depart_option_list_item(self, node): |
|
1134 self.body.append('</tr>\n') |
|
1135 |
|
1136 def visit_option_string(self, node): |
|
1137 pass |
|
1138 |
|
1139 def depart_option_string(self, node): |
|
1140 pass |
|
1141 |
|
1142 def visit_organization(self, node): |
|
1143 self.visit_docinfo_item(node, 'organization') |
|
1144 |
|
1145 def depart_organization(self, node): |
|
1146 self.depart_docinfo_item() |
|
1147 |
|
1148 def should_be_compact_paragraph(self, node): |
|
1149 """ |
|
1150 Determine if the <p> tags around paragraph ``node`` can be omitted. |
|
1151 """ |
|
1152 if (isinstance(node.parent, nodes.document) or |
|
1153 isinstance(node.parent, nodes.compound)): |
|
1154 # Never compact paragraphs in document or compound. |
|
1155 return 0 |
|
1156 for key, value in node.attlist(): |
|
1157 if (node.is_not_default(key) and |
|
1158 not (key == 'classes' and value in |
|
1159 ([], ['first'], ['last'], ['first', 'last']))): |
|
1160 # Attribute which needs to survive. |
|
1161 return 0 |
|
1162 first = isinstance(node.parent[0], nodes.label) # skip label |
|
1163 for child in node.parent.children[first:]: |
|
1164 # only first paragraph can be compact |
|
1165 if isinstance(child, nodes.Invisible): |
|
1166 continue |
|
1167 if child is node: |
|
1168 break |
|
1169 return 0 |
|
1170 parent_length = len([n for n in node.parent if not isinstance( |
|
1171 n, (nodes.Invisible, nodes.label))]) |
|
1172 if ( self.compact_simple |
|
1173 or self.compact_field_list |
|
1174 or self.compact_p and parent_length == 1): |
|
1175 return 1 |
|
1176 return 0 |
|
1177 |
|
1178 def visit_paragraph(self, node): |
|
1179 if self.should_be_compact_paragraph(node): |
|
1180 self.context.append('') |
|
1181 else: |
|
1182 self.body.append(self.starttag(node, 'p', '')) |
|
1183 self.context.append('</p>\n') |
|
1184 |
|
1185 def depart_paragraph(self, node): |
|
1186 self.body.append(self.context.pop()) |
|
1187 |
|
1188 def visit_problematic(self, node): |
|
1189 if node.hasattr('refid'): |
|
1190 self.body.append('<a href="#%s">' % node['refid']) |
|
1191 self.context.append('</a>') |
|
1192 else: |
|
1193 self.context.append('') |
|
1194 self.body.append(self.starttag(node, 'span', '', CLASS='problematic')) |
|
1195 |
|
1196 def depart_problematic(self, node): |
|
1197 self.body.append('</span>') |
|
1198 self.body.append(self.context.pop()) |
|
1199 |
|
1200 def visit_raw(self, node): |
|
1201 if 'html' in node.get('format', '').split(): |
|
1202 t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div' |
|
1203 if node['classes']: |
|
1204 self.body.append(self.starttag(node, t, suffix='')) |
|
1205 self.body.append(node.astext()) |
|
1206 if node['classes']: |
|
1207 self.body.append('</%s>' % t) |
|
1208 # Keep non-HTML raw text out of output: |
|
1209 raise nodes.SkipNode |
|
1210 |
|
1211 def visit_reference(self, node): |
|
1212 atts = {'class': 'reference'} |
|
1213 if node.has_key('refuri'): |
|
1214 atts['href'] = node['refuri'] |
|
1215 if ( self.settings.cloak_email_addresses |
|
1216 and atts['href'].startswith('mailto:')): |
|
1217 atts['href'] = self.cloak_mailto(atts['href']) |
|
1218 self.in_mailto = 1 |
|
1219 atts['class'] += ' external' |
|
1220 else: |
|
1221 assert node.has_key('refid'), \ |
|
1222 'References must have "refuri" or "refid" attribute.' |
|
1223 atts['href'] = '#' + node['refid'] |
|
1224 atts['class'] += ' internal' |
|
1225 if not isinstance(node.parent, nodes.TextElement): |
|
1226 assert len(node) == 1 and isinstance(node[0], nodes.image) |
|
1227 atts['class'] += ' image-reference' |
|
1228 self.body.append(self.starttag(node, 'a', '', **atts)) |
|
1229 |
|
1230 def depart_reference(self, node): |
|
1231 self.body.append('</a>') |
|
1232 if not isinstance(node.parent, nodes.TextElement): |
|
1233 self.body.append('\n') |
|
1234 self.in_mailto = 0 |
|
1235 |
|
1236 def visit_revision(self, node): |
|
1237 self.visit_docinfo_item(node, 'revision', meta=None) |
|
1238 |
|
1239 def depart_revision(self, node): |
|
1240 self.depart_docinfo_item() |
|
1241 |
|
1242 def visit_row(self, node): |
|
1243 self.body.append(self.starttag(node, 'tr', '')) |
|
1244 node.column = 0 |
|
1245 |
|
1246 def depart_row(self, node): |
|
1247 self.body.append('</tr>\n') |
|
1248 |
|
1249 def visit_rubric(self, node): |
|
1250 self.body.append(self.starttag(node, 'p', '', CLASS='rubric')) |
|
1251 |
|
1252 def depart_rubric(self, node): |
|
1253 self.body.append('</p>\n') |
|
1254 |
|
1255 def visit_section(self, node): |
|
1256 self.section_level += 1 |
|
1257 self.body.append( |
|
1258 self.starttag(node, 'div', CLASS='section')) |
|
1259 |
|
1260 def depart_section(self, node): |
|
1261 self.section_level -= 1 |
|
1262 self.body.append('</div>\n') |
|
1263 |
|
1264 def visit_sidebar(self, node): |
|
1265 self.body.append( |
|
1266 self.starttag(node, 'div', CLASS='sidebar')) |
|
1267 self.set_first_last(node) |
|
1268 self.in_sidebar = 1 |
|
1269 |
|
1270 def depart_sidebar(self, node): |
|
1271 self.body.append('</div>\n') |
|
1272 self.in_sidebar = None |
|
1273 |
|
1274 def visit_status(self, node): |
|
1275 self.visit_docinfo_item(node, 'status', meta=None) |
|
1276 |
|
1277 def depart_status(self, node): |
|
1278 self.depart_docinfo_item() |
|
1279 |
|
1280 def visit_strong(self, node): |
|
1281 self.body.append('<strong>') |
|
1282 |
|
1283 def depart_strong(self, node): |
|
1284 self.body.append('</strong>') |
|
1285 |
|
1286 def visit_subscript(self, node): |
|
1287 self.body.append(self.starttag(node, 'sub', '')) |
|
1288 |
|
1289 def depart_subscript(self, node): |
|
1290 self.body.append('</sub>') |
|
1291 |
|
1292 def visit_substitution_definition(self, node): |
|
1293 """Internal only.""" |
|
1294 raise nodes.SkipNode |
|
1295 |
|
1296 def visit_substitution_reference(self, node): |
|
1297 self.unimplemented_visit(node) |
|
1298 |
|
1299 def visit_subtitle(self, node): |
|
1300 if isinstance(node.parent, nodes.sidebar): |
|
1301 self.body.append(self.starttag(node, 'p', '', |
|
1302 CLASS='sidebar-subtitle')) |
|
1303 self.context.append('</p>\n') |
|
1304 elif isinstance(node.parent, nodes.document): |
|
1305 self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle')) |
|
1306 self.context.append('</h2>\n') |
|
1307 self.in_document_title = len(self.body) |
|
1308 elif isinstance(node.parent, nodes.section): |
|
1309 tag = 'h%s' % (self.section_level + self.initial_header_level - 1) |
|
1310 self.body.append( |
|
1311 self.starttag(node, tag, '', CLASS='section-subtitle') + |
|
1312 self.starttag({}, 'span', '', CLASS='section-subtitle')) |
|
1313 self.context.append('</span></%s>\n' % tag) |
|
1314 |
|
1315 def depart_subtitle(self, node): |
|
1316 self.body.append(self.context.pop()) |
|
1317 if self.in_document_title: |
|
1318 self.subtitle = self.body[self.in_document_title:-1] |
|
1319 self.in_document_title = 0 |
|
1320 self.body_pre_docinfo.extend(self.body) |
|
1321 self.html_subtitle.extend(self.body) |
|
1322 del self.body[:] |
|
1323 |
|
1324 def visit_superscript(self, node): |
|
1325 self.body.append(self.starttag(node, 'sup', '')) |
|
1326 |
|
1327 def depart_superscript(self, node): |
|
1328 self.body.append('</sup>') |
|
1329 |
|
1330 def visit_system_message(self, node): |
|
1331 self.body.append(self.starttag(node, 'div', CLASS='system-message')) |
|
1332 self.body.append('<p class="system-message-title">') |
|
1333 backref_text = '' |
|
1334 if len(node['backrefs']): |
|
1335 backrefs = node['backrefs'] |
|
1336 if len(backrefs) == 1: |
|
1337 backref_text = ('; <em><a href="#%s">backlink</a></em>' |
|
1338 % backrefs[0]) |
|
1339 else: |
|
1340 i = 1 |
|
1341 backlinks = [] |
|
1342 for backref in backrefs: |
|
1343 backlinks.append('<a href="#%s">%s</a>' % (backref, i)) |
|
1344 i += 1 |
|
1345 backref_text = ('; <em>backlinks: %s</em>' |
|
1346 % ', '.join(backlinks)) |
|
1347 if node.hasattr('line'): |
|
1348 line = ', line %s' % node['line'] |
|
1349 else: |
|
1350 line = '' |
|
1351 self.body.append('System Message: %s/%s ' |
|
1352 '(<tt class="docutils">%s</tt>%s)%s</p>\n' |
|
1353 % (node['type'], node['level'], |
|
1354 self.encode(node['source']), line, backref_text)) |
|
1355 |
|
1356 def depart_system_message(self, node): |
|
1357 self.body.append('</div>\n') |
|
1358 |
|
1359 def visit_table(self, node): |
|
1360 self.body.append( |
|
1361 self.starttag(node, 'table', CLASS='docutils', border="1")) |
|
1362 |
|
1363 def depart_table(self, node): |
|
1364 self.body.append('</table>\n') |
|
1365 |
|
1366 def visit_target(self, node): |
|
1367 if not (node.has_key('refuri') or node.has_key('refid') |
|
1368 or node.has_key('refname')): |
|
1369 self.body.append(self.starttag(node, 'span', '', CLASS='target')) |
|
1370 self.context.append('</span>') |
|
1371 else: |
|
1372 self.context.append('') |
|
1373 |
|
1374 def depart_target(self, node): |
|
1375 self.body.append(self.context.pop()) |
|
1376 |
|
1377 def visit_tbody(self, node): |
|
1378 self.write_colspecs() |
|
1379 self.body.append(self.context.pop()) # '</colgroup>\n' or '' |
|
1380 self.body.append(self.starttag(node, 'tbody', valign='top')) |
|
1381 |
|
1382 def depart_tbody(self, node): |
|
1383 self.body.append('</tbody>\n') |
|
1384 |
|
1385 def visit_term(self, node): |
|
1386 self.body.append(self.starttag(node, 'dt', '')) |
|
1387 |
|
1388 def depart_term(self, node): |
|
1389 """ |
|
1390 Leave the end tag to `self.visit_definition()`, in case there's a |
|
1391 classifier. |
|
1392 """ |
|
1393 pass |
|
1394 |
|
1395 def visit_tgroup(self, node): |
|
1396 # Mozilla needs <colgroup>: |
|
1397 self.body.append(self.starttag(node, 'colgroup')) |
|
1398 # Appended by thead or tbody: |
|
1399 self.context.append('</colgroup>\n') |
|
1400 node.stubs = [] |
|
1401 |
|
1402 def depart_tgroup(self, node): |
|
1403 pass |
|
1404 |
|
1405 def visit_thead(self, node): |
|
1406 self.write_colspecs() |
|
1407 self.body.append(self.context.pop()) # '</colgroup>\n' |
|
1408 # There may or may not be a <thead>; this is for <tbody> to use: |
|
1409 self.context.append('') |
|
1410 self.body.append(self.starttag(node, 'thead', valign='bottom')) |
|
1411 |
|
1412 def depart_thead(self, node): |
|
1413 self.body.append('</thead>\n') |
|
1414 |
|
1415 def visit_title(self, node): |
|
1416 """Only 6 section levels are supported by HTML.""" |
|
1417 check_id = 0 |
|
1418 close_tag = '</p>\n' |
|
1419 if isinstance(node.parent, nodes.topic): |
|
1420 self.body.append( |
|
1421 self.starttag(node, 'p', '', CLASS='topic-title first')) |
|
1422 elif isinstance(node.parent, nodes.sidebar): |
|
1423 self.body.append( |
|
1424 self.starttag(node, 'p', '', CLASS='sidebar-title')) |
|
1425 elif isinstance(node.parent, nodes.Admonition): |
|
1426 self.body.append( |
|
1427 self.starttag(node, 'p', '', CLASS='admonition-title')) |
|
1428 elif isinstance(node.parent, nodes.table): |
|
1429 self.body.append( |
|
1430 self.starttag(node, 'caption', '')) |
|
1431 close_tag = '</caption>\n' |
|
1432 elif isinstance(node.parent, nodes.document): |
|
1433 self.body.append(self.starttag(node, 'h1', '', CLASS='title')) |
|
1434 close_tag = '</h1>\n' |
|
1435 self.in_document_title = len(self.body) |
|
1436 else: |
|
1437 assert isinstance(node.parent, nodes.section) |
|
1438 h_level = self.section_level + self.initial_header_level - 1 |
|
1439 atts = {} |
|
1440 if (len(node.parent) >= 2 and |
|
1441 isinstance(node.parent[1], nodes.subtitle)): |
|
1442 atts['CLASS'] = 'with-subtitle' |
|
1443 self.body.append( |
|
1444 self.starttag(node, 'h%s' % h_level, '', **atts)) |
|
1445 atts = {} |
|
1446 if node.hasattr('refid'): |
|
1447 atts['class'] = 'toc-backref' |
|
1448 atts['href'] = '#' + node['refid'] |
|
1449 if atts: |
|
1450 self.body.append(self.starttag({}, 'a', '', **atts)) |
|
1451 close_tag = '</a></h%s>\n' % (h_level) |
|
1452 else: |
|
1453 close_tag = '</h%s>\n' % (h_level) |
|
1454 self.context.append(close_tag) |
|
1455 |
|
1456 def depart_title(self, node): |
|
1457 self.body.append(self.context.pop()) |
|
1458 if self.in_document_title: |
|
1459 self.title = self.body[self.in_document_title:-1] |
|
1460 self.in_document_title = 0 |
|
1461 self.body_pre_docinfo.extend(self.body) |
|
1462 self.html_title.extend(self.body) |
|
1463 del self.body[:] |
|
1464 |
|
1465 def visit_title_reference(self, node): |
|
1466 self.body.append(self.starttag(node, 'cite', '')) |
|
1467 |
|
1468 def depart_title_reference(self, node): |
|
1469 self.body.append('</cite>') |
|
1470 |
|
1471 def visit_topic(self, node): |
|
1472 self.body.append(self.starttag(node, 'div', CLASS='topic')) |
|
1473 self.topic_classes = node['classes'] |
|
1474 |
|
1475 def depart_topic(self, node): |
|
1476 self.body.append('</div>\n') |
|
1477 self.topic_classes = [] |
|
1478 |
|
1479 def visit_transition(self, node): |
|
1480 self.body.append(self.emptytag(node, 'hr', CLASS='docutils')) |
|
1481 |
|
1482 def depart_transition(self, node): |
|
1483 pass |
|
1484 |
|
1485 def visit_version(self, node): |
|
1486 self.visit_docinfo_item(node, 'version', meta=None) |
|
1487 |
|
1488 def depart_version(self, node): |
|
1489 self.depart_docinfo_item() |
|
1490 |
|
1491 def unimplemented_visit(self, node): |
|
1492 raise NotImplementedError('visiting unimplemented node type: %s' |
|
1493 % node.__class__.__name__) |
|
1494 |
|
1495 |
|
1496 class SimpleListChecker(nodes.GenericNodeVisitor): |
|
1497 |
|
1498 """ |
|
1499 Raise `nodes.NodeFound` if non-simple list item is encountered. |
|
1500 |
|
1501 Here "simple" means a list item containing nothing other than a single |
|
1502 paragraph, a simple list, or a paragraph followed by a simple list. |
|
1503 """ |
|
1504 |
|
1505 def default_visit(self, node): |
|
1506 raise nodes.NodeFound |
|
1507 |
|
1508 def visit_bullet_list(self, node): |
|
1509 pass |
|
1510 |
|
1511 def visit_enumerated_list(self, node): |
|
1512 pass |
|
1513 |
|
1514 def visit_list_item(self, node): |
|
1515 children = [] |
|
1516 for child in node.children: |
|
1517 if not isinstance(child, nodes.Invisible): |
|
1518 children.append(child) |
|
1519 if (children and isinstance(children[0], nodes.paragraph) |
|
1520 and (isinstance(children[-1], nodes.bullet_list) |
|
1521 or isinstance(children[-1], nodes.enumerated_list))): |
|
1522 children.pop() |
|
1523 if len(children) <= 1: |
|
1524 return |
|
1525 else: |
|
1526 raise nodes.NodeFound |
|
1527 |
|
1528 def visit_paragraph(self, node): |
|
1529 raise nodes.SkipNode |
|
1530 |
|
1531 def invisible_visit(self, node): |
|
1532 """Invisible nodes should be ignored.""" |
|
1533 raise nodes.SkipNode |
|
1534 |
|
1535 visit_comment = invisible_visit |
|
1536 visit_substitution_definition = invisible_visit |
|
1537 visit_target = invisible_visit |
|
1538 visit_pending = invisible_visit |