~ubuntu-branches/ubuntu/karmic/python-docutils/karmic

« back to all changes in this revision

Viewing changes to docutils/writers/html4css1/__init__.py

  • Committer: Bazaar Package Importer
  • Author(s): martin f. krafft
  • Date: 2006-07-10 11:45:05 UTC
  • mfrom: (2.1.4 edgy)
  • Revision ID: james.westby@ubuntu.com-20060710114505-otkhqcslevewxmz5
Tags: 0.4-3
Added build dependency on python-central (closes: #377580).

Show diffs side-by-side

added added

removed removed

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