buildframework/helium/external/python/lib/common/docutils-0.5-py2.5.egg/docutils/writers/html4css1/__init__.py
changeset 179 d8ac696cc51f
equal deleted inserted replaced
1:be27ed110b50 179:d8ac696cc51f
       
     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("&", "&amp;")
       
   311         text = text.replace("<", "&lt;")
       
   312         text = text.replace('"', "&quot;")
       
   313         text = text.replace(">", "&gt;")
       
   314         text = text.replace("@", "&#64;") # may thwart some address harvesters
       
   315         # Replace the non-breaking space character with the HTML entity:
       
   316         text = text.replace(u'\u00a0', "&nbsp;")
       
   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 "&#64;" by the `encode` method.)
       
   330         addr = addr.replace('&#64;', '<span>&#64;</span>')
       
   331         addr = addr.replace('.', '<span>&#46;</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', '&#37;&#52;&#48;')
       
   341             encoded = encoded.replace('.', '&#46;')
       
   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': ('&mdash;', ''),
       
   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('&nbsp;')
       
   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>&nbsp;</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('&nbsp;' * (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>&nbsp;</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