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