1# $Id: __init__.py 8638 2021-03-20 23:47:07Z milde $
2# Author: David Goodger
3# Maintainer: docutils-develop@lists.sourceforge.net
4# Copyright: This module has been placed in the public domain.
5
6"""
7Simple HyperText Markup Language document tree Writer.
8
9The output conforms to the XHTML version 1.0 Transitional DTD
10(*almost* strict).  The output contains a minimum of formatting
11information.  The cascading style sheet "html4css1.css" is required
12for proper viewing with a modern graphical browser.
13"""
14
15__docformat__ = 'reStructuredText'
16
17import os.path
18import re
19import sys
20import docutils
21from docutils import frontend, nodes, writers, io
22from docutils.transforms import writer_aux
23from docutils.writers import _html_base
24from docutils.writers._html_base import PIL, url2pathname
25
26class Writer(writers._html_base.Writer):
27
28    supported = ('html', 'html4', 'html4css1', 'xhtml', 'xhtml10')
29    """Formats this writer supports."""
30
31    default_stylesheets = ['html4css1.css']
32    default_stylesheet_dirs = ['.',
33        os.path.abspath(os.path.dirname(__file__)),
34        # for math.css
35        os.path.abspath(os.path.join(
36            os.path.dirname(os.path.dirname(__file__)), 'html5_polyglot'))
37       ]
38
39    default_template = 'template.txt'
40    default_template_path = os.path.join(
41        os.path.dirname(os.path.abspath(__file__)), default_template)
42
43    settings_spec = (
44        'HTML-Specific Options',
45        None,
46        (('Specify the template file (UTF-8 encoded).  Default is "%s".'
47          % default_template_path,
48          ['--template'],
49          {'default': default_template_path, 'metavar': '<file>'}),
50         ('Comma separated list of stylesheet URLs. '
51          'Overrides previous --stylesheet and --stylesheet-path settings.',
52          ['--stylesheet'],
53          {'metavar': '<URL[,URL,...]>', 'overrides': 'stylesheet_path',
54           'validator': frontend.validate_comma_separated_list}),
55         ('Comma separated list of stylesheet paths. '
56          'Relative paths are expanded if a matching file is found in '
57          'the --stylesheet-dirs. With --link-stylesheet, '
58          'the path is rewritten relative to the output HTML file. '
59          'Default: "%s"' % ','.join(default_stylesheets),
60          ['--stylesheet-path'],
61          {'metavar': '<file[,file,...]>', 'overrides': 'stylesheet',
62           'validator': frontend.validate_comma_separated_list,
63           'default': default_stylesheets}),
64         ('Embed the stylesheet(s) in the output HTML file.  The stylesheet '
65          'files must be accessible during processing. This is the default.',
66          ['--embed-stylesheet'],
67          {'default': 1, 'action': 'store_true',
68           'validator': frontend.validate_boolean}),
69         ('Link to the stylesheet(s) in the output HTML file. '
70          'Default: embed stylesheets.',
71          ['--link-stylesheet'],
72          {'dest': 'embed_stylesheet', 'action': 'store_false'}),
73         ('Comma-separated list of directories where stylesheets are found. '
74          'Used by --stylesheet-path when expanding relative path arguments. '
75          'Default: "%s"' % default_stylesheet_dirs,
76          ['--stylesheet-dirs'],
77          {'metavar': '<dir[,dir,...]>',
78           'validator': frontend.validate_comma_separated_list,
79           'default': default_stylesheet_dirs}),
80         ('Specify the initial header level.  Default is 1 for "<h1>".  '
81          'Does not affect document title & subtitle (see --no-doc-title).',
82          ['--initial-header-level'],
83          {'choices': '1 2 3 4 5 6'.split(), 'default': '1',
84           'metavar': '<level>'}),
85         ('Specify the maximum width (in characters) for one-column field '
86          'names.  Longer field names will span an entire row of the table '
87          'used to render the field list.  Default is 14 characters.  '
88          'Use 0 for "no limit".',
89          ['--field-name-limit'],
90          {'default': 14, 'metavar': '<level>',
91           'validator': frontend.validate_nonnegative_int}),
92         ('Specify the maximum width (in characters) for options in option '
93          'lists.  Longer options will span an entire row of the table used '
94          'to render the option list.  Default is 14 characters.  '
95          'Use 0 for "no limit".',
96          ['--option-limit'],
97          {'default': 14, 'metavar': '<level>',
98           'validator': frontend.validate_nonnegative_int}),
99         ('Format for footnote references: one of "superscript" or '
100          '"brackets".  Default is "brackets".',
101          ['--footnote-references'],
102          {'choices': ['superscript', 'brackets'], 'default': 'brackets',
103           'metavar': '<format>',
104           'overrides': 'trim_footnote_reference_space'}),
105         ('Format for block quote attributions: one of "dash" (em-dash '
106          'prefix), "parentheses"/"parens", or "none".  Default is "dash".',
107          ['--attribution'],
108          {'choices': ['dash', 'parentheses', 'parens', 'none'],
109           'default': 'dash', 'metavar': '<format>'}),
110         ('Remove extra vertical whitespace between items of "simple" bullet '
111          'lists and enumerated lists.  Default: enabled.',
112          ['--compact-lists'],
113          {'default': 1, 'action': 'store_true',
114           'validator': frontend.validate_boolean}),
115         ('Disable compact simple bullet and enumerated lists.',
116          ['--no-compact-lists'],
117          {'dest': 'compact_lists', 'action': 'store_false'}),
118         ('Remove extra vertical whitespace between items of simple field '
119          'lists.  Default: enabled.',
120          ['--compact-field-lists'],
121          {'default': 1, 'action': 'store_true',
122           'validator': frontend.validate_boolean}),
123         ('Disable compact simple field lists.',
124          ['--no-compact-field-lists'],
125          {'dest': 'compact_field_lists', 'action': 'store_false'}),
126         ('Embed images in the output HTML file, if the image '
127          'files are accessible during processing.',
128          ['--embed-images'],
129          {'default': 0, 'action': 'store_true',
130           'validator': frontend.validate_boolean}),
131         ('Link to images in the output HTML file. '
132          'This is the default.',
133          ['--link-images'],
134          {'dest': 'embed_images', 'action': 'store_false'}),
135         ('Added to standard table classes. '
136          'Defined styles: "borderless". Default: ""',
137          ['--table-style'],
138          {'default': ''}),
139         ('Math output format, one of "MathML", "HTML", "MathJax" '
140          'or "LaTeX". Default: "HTML math.css"',
141          ['--math-output'],
142          {'default': 'HTML math.css'}),
143         ('Omit the XML declaration.  Use with caution.',
144          ['--no-xml-declaration'],
145          {'dest': 'xml_declaration', 'default': 1, 'action': 'store_false',
146           'validator': frontend.validate_boolean}),
147         ('Obfuscate email addresses to confuse harvesters while still '
148          'keeping email links usable with standards-compliant browsers.',
149          ['--cloak-email-addresses'],
150          {'action': 'store_true', 'validator': frontend.validate_boolean}),))
151
152    config_section = 'html4css1 writer'
153
154    def __init__(self):
155        self.parts = {}
156        self.translator_class = HTMLTranslator
157
158
159class HTMLTranslator(writers._html_base.HTMLTranslator):
160    """
161    The html4css1 writer has been optimized to produce visually compact
162    lists (less vertical whitespace).  HTML's mixed content models
163    allow list items to contain "<li><p>body elements</p></li>" or
164    "<li>just text</li>" or even "<li>text<p>and body
165    elements</p>combined</li>", each with different effects.  It would
166    be best to stick with strict body elements in list items, but they
167    affect vertical spacing in older browsers (although they really
168    shouldn't).
169    The html5_polyglot writer solves this using CSS2.
170
171    Here is an outline of the optimization:
172
173    - Check for and omit <p> tags in "simple" lists: list items
174      contain either a single paragraph, a nested simple list, or a
175      paragraph followed by a nested simple list.  This means that
176      this list can be compact:
177
178          - Item 1.
179          - Item 2.
180
181      But this list cannot be compact:
182
183          - Item 1.
184
185            This second paragraph forces space between list items.
186
187          - Item 2.
188
189    - In non-list contexts, omit <p> tags on a paragraph if that
190      paragraph is the only child of its parent (footnotes & citations
191      are allowed a label first).
192
193    - Regardless of the above, in definitions, table cells, field bodies,
194      option descriptions, and list items, mark the first child with
195      'class="first"' and the last child with 'class="last"'.  The stylesheet
196      sets the margins (top & bottom respectively) to 0 for these elements.
197
198    The ``no_compact_lists`` setting (``--no-compact-lists`` command-line
199    option) disables list whitespace optimization.
200    """
201
202    # The following definitions are required for display in browsers limited
203    # to CSS1 or backwards compatible behaviour of the writer:
204
205    doctype = (
206        '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"'
207        ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n')
208
209    content_type = ('<meta http-equiv="Content-Type"'
210                    ' content="text/html; charset=%s" />\n')
211    content_type_mathml = ('<meta http-equiv="Content-Type"'
212                           ' content="application/xhtml+xml; charset=%s" />\n')
213
214    # encode also non-breaking space
215    special_characters = dict(_html_base.HTMLTranslator.special_characters)
216    special_characters[0xa0] = u'&nbsp;'
217
218    # use character reference for dash (not valid in HTML5)
219    attribution_formats = {'dash': ('&mdash;', ''),
220                           'parentheses': ('(', ')'),
221                           'parens': ('(', ')'),
222                           'none': ('', '')}
223
224    # ersatz for first/last pseudo-classes missing in CSS1
225    def set_first_last(self, node):
226        self.set_class_on_child(node, 'first', 0)
227        self.set_class_on_child(node, 'last', -1)
228
229    # add newline after opening tag
230    def visit_address(self, node):
231        self.visit_docinfo_item(node, 'address', meta=False)
232        self.body.append(self.starttag(node, 'pre', CLASS='address'))
233
234
235    # ersatz for first/last pseudo-classes
236    def visit_admonition(self, node):
237        node['classes'].insert(0, 'admonition')
238        self.body.append(self.starttag(node, 'div'))
239        self.set_first_last(node)
240
241    # author, authors: use <br> instead of paragraphs
242    def visit_author(self, node):
243        if isinstance(node.parent, nodes.authors):
244            if self.author_in_authors:
245                self.body.append('\n<br />')
246        else:
247            self.visit_docinfo_item(node, 'author')
248
249    def depart_author(self, node):
250        if isinstance(node.parent, nodes.authors):
251            self.author_in_authors = True
252        else:
253            self.depart_docinfo_item()
254
255    def visit_authors(self, node):
256        self.visit_docinfo_item(node, 'authors')
257        self.author_in_authors = False  # initialize
258
259    def depart_authors(self, node):
260        self.depart_docinfo_item()
261
262    # use "width" argument insted of "style: 'width'":
263    def visit_colspec(self, node):
264        self.colspecs.append(node)
265        # "stubs" list is an attribute of the tgroup element:
266        node.parent.stubs.append(node.attributes.get('stub'))
267    #
268    def depart_colspec(self, node):
269        # write out <colgroup> when all colspecs are processed
270        if isinstance(node.next_node(descend=False, siblings=True),
271                      nodes.colspec):
272            return
273        if 'colwidths-auto' in node.parent.parent['classes'] or (
274            'colwidths-auto' in self.settings.table_style and
275            ('colwidths-given' not in node.parent.parent['classes'])):
276            return
277        total_width = sum(node['colwidth'] for node in self.colspecs)
278        self.body.append(self.starttag(node, 'colgroup'))
279        for node in self.colspecs:
280            colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5)
281            self.body.append(self.emptytag(node, 'col',
282                                           width='%i%%' % colwidth))
283        self.body.append('</colgroup>\n')
284
285    # Compact lists:
286    # exclude definition lists and field lists (non-compact by default)
287
288    def is_compactable(self, node):
289        return ('compact' in node['classes']
290                or (self.settings.compact_lists
291                    and 'open' not in node['classes']
292                    and (self.compact_simple
293                         or self.topic_classes == ['contents']
294                         # TODO: self.in_contents
295                         or self.check_simple_list(node))))
296
297    # citations: Use table for bibliographic references.
298    def visit_citation(self, node):
299        self.body.append(self.starttag(node, 'table',
300                                       CLASS='docutils citation',
301                                       frame="void", rules="none"))
302        self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
303                         '<tbody valign="top">\n'
304                         '<tr>')
305        self.footnote_backrefs(node)
306
307    def depart_citation(self, node):
308        self.body.append('</td></tr>\n'
309                         '</tbody>\n</table>\n')
310
311    # insert classifier-delimiter (not required with CSS2)
312    def visit_classifier(self, node):
313        self.body.append(' <span class="classifier-delimiter">:</span> ')
314        self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
315
316    # ersatz for first/last pseudo-classes
317    def visit_definition(self, node):
318        self.body.append('</dt>\n')
319        self.body.append(self.starttag(node, 'dd', ''))
320        self.set_first_last(node)
321
322    # don't add "simple" class value
323    def visit_definition_list(self, node):
324        self.body.append(self.starttag(node, 'dl', CLASS='docutils'))
325
326    # use a table for description lists
327    def visit_description(self, node):
328        self.body.append(self.starttag(node, 'td', ''))
329        self.set_first_last(node)
330
331    def depart_description(self, node):
332        self.body.append('</td>')
333
334    # use table for docinfo
335    def visit_docinfo(self, node):
336        self.context.append(len(self.body))
337        self.body.append(self.starttag(node, 'table',
338                                       CLASS='docinfo',
339                                       frame="void", rules="none"))
340        self.body.append('<col class="docinfo-name" />\n'
341                         '<col class="docinfo-content" />\n'
342                         '<tbody valign="top">\n')
343        self.in_docinfo = True
344
345    def depart_docinfo(self, node):
346        self.body.append('</tbody>\n</table>\n')
347        self.in_docinfo = False
348        start = self.context.pop()
349        self.docinfo = self.body[start:]
350        self.body = []
351
352    def visit_docinfo_item(self, node, name, meta=True):
353        if meta:
354            meta_tag = '<meta name="%s" content="%s" />\n' \
355                       % (name, self.attval(node.astext()))
356            self.add_meta(meta_tag)
357        self.body.append(self.starttag(node, 'tr', ''))
358        self.body.append('<th class="docinfo-name">%s:</th>\n<td>'
359                         % self.language.labels[name])
360        if len(node):
361            if isinstance(node[0], nodes.Element):
362                node[0]['classes'].append('first')
363            if isinstance(node[-1], nodes.Element):
364                node[-1]['classes'].append('last')
365
366    def depart_docinfo_item(self):
367        self.body.append('</td></tr>\n')
368
369    # add newline after opening tag
370    def visit_doctest_block(self, node):
371        self.body.append(self.starttag(node, 'pre', CLASS='doctest-block'))
372
373    # insert an NBSP into empty cells, ersatz for first/last
374    def visit_entry(self, node):
375        writers._html_base.HTMLTranslator.visit_entry(self, node)
376        if len(node) == 0:              # empty cell
377            self.body.append('&nbsp;')
378        self.set_first_last(node)
379
380    # ersatz for first/last pseudo-classes
381    def visit_enumerated_list(self, node):
382        """
383        The 'start' attribute does not conform to HTML 4.01's strict.dtd, but
384        cannot be emulated in CSS1 (HTML 5 reincludes it).
385        """
386        atts = {}
387        if 'start' in node:
388            atts['start'] = node['start']
389        if 'enumtype' in node:
390            atts['class'] = node['enumtype']
391        # @@@ To do: prefix, suffix. How? Change prefix/suffix to a
392        # single "format" attribute? Use CSS2?
393        old_compact_simple = self.compact_simple
394        self.context.append((self.compact_simple, self.compact_p))
395        self.compact_p = None
396        self.compact_simple = self.is_compactable(node)
397        if self.compact_simple and not old_compact_simple:
398            atts['class'] = (atts.get('class', '') + ' simple').strip()
399        self.body.append(self.starttag(node, 'ol', **atts))
400
401    def depart_enumerated_list(self, node):
402        self.compact_simple, self.compact_p = self.context.pop()
403        self.body.append('</ol>\n')
404
405    # use table for field-list:
406    def visit_field(self, node):
407        self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
408
409    def depart_field(self, node):
410        self.body.append('</tr>\n')
411
412    def visit_field_body(self, node):
413        self.body.append(self.starttag(node, 'td', '', CLASS='field-body'))
414        self.set_class_on_child(node, 'first', 0)
415        field = node.parent
416        if (self.compact_field_list or
417            isinstance(field.parent, nodes.docinfo) or
418            field.parent.index(field) == len(field.parent) - 1):
419            # If we are in a compact list, the docinfo, or if this is
420            # the last field of the field list, do not add vertical
421            # space after last element.
422            self.set_class_on_child(node, 'last', -1)
423
424    def depart_field_body(self, node):
425        self.body.append('</td>\n')
426
427    def visit_field_list(self, node):
428        self.context.append((self.compact_field_list, self.compact_p))
429        self.compact_p = None
430        if 'compact' in node['classes']:
431            self.compact_field_list = True
432        elif (self.settings.compact_field_lists
433              and 'open' not in node['classes']):
434            self.compact_field_list = True
435        if self.compact_field_list:
436            for field in node:
437                field_body = field[-1]
438                assert isinstance(field_body, nodes.field_body)
439                children = [n for n in field_body
440                            if not isinstance(n, nodes.Invisible)]
441                if not (len(children) == 0 or
442                        len(children) == 1 and
443                        isinstance(children[0],
444                                   (nodes.paragraph, nodes.line_block))):
445                    self.compact_field_list = False
446                    break
447        self.body.append(self.starttag(node, 'table', frame='void',
448                                       rules='none',
449                                       CLASS='docutils field-list'))
450        self.body.append('<col class="field-name" />\n'
451                         '<col class="field-body" />\n'
452                         '<tbody valign="top">\n')
453
454    def depart_field_list(self, node):
455        self.body.append('</tbody>\n</table>\n')
456        self.compact_field_list, self.compact_p = self.context.pop()
457
458    def visit_field_name(self, node):
459        atts = {}
460        if self.in_docinfo:
461            atts['class'] = 'docinfo-name'
462        else:
463            atts['class'] = 'field-name'
464        if ( self.settings.field_name_limit
465             and len(node.astext()) > self.settings.field_name_limit):
466            atts['colspan'] = 2
467            self.context.append('</tr>\n'
468                                + self.starttag(node.parent, 'tr', '',
469                                                CLASS='field')
470                                + '<td>&nbsp;</td>')
471        else:
472            self.context.append('')
473        self.body.append(self.starttag(node, 'th', '', **atts))
474
475    def depart_field_name(self, node):
476        self.body.append(':</th>')
477        self.body.append(self.context.pop())
478
479    # use table for footnote text
480    def visit_footnote(self, node):
481        self.body.append(self.starttag(node, 'table',
482                                       CLASS='docutils footnote',
483                                       frame="void", rules="none"))
484        self.body.append('<colgroup><col class="label" /><col /></colgroup>\n'
485                         '<tbody valign="top">\n'
486                         '<tr>')
487        self.footnote_backrefs(node)
488
489    def footnote_backrefs(self, node):
490        backlinks = []
491        backrefs = node['backrefs']
492        if self.settings.footnote_backlinks and backrefs:
493            if len(backrefs) == 1:
494                self.context.append('')
495                self.context.append('</a>')
496                self.context.append('<a class="fn-backref" href="#%s">'
497                                    % backrefs[0])
498            else:
499                for (i, backref) in enumerate(backrefs, 1):
500                    backlinks.append('<a class="fn-backref" href="#%s">%s</a>'
501                                     % (backref, i))
502                self.context.append('<em>(%s)</em> ' % ', '.join(backlinks))
503                self.context += ['', '']
504        else:
505            self.context.append('')
506            self.context += ['', '']
507        # If the node does not only consist of a label.
508        if len(node) > 1:
509            # If there are preceding backlinks, we do not set class
510            # 'first', because we need to retain the top-margin.
511            if not backlinks:
512                node[1]['classes'].append('first')
513            node[-1]['classes'].append('last')
514
515    def depart_footnote(self, node):
516        self.body.append('</td></tr>\n'
517                         '</tbody>\n</table>\n')
518
519    # insert markers in text as pseudo-classes are not supported in CSS1:
520    def visit_footnote_reference(self, node):
521        href = '#' + node['refid']
522        format = self.settings.footnote_references
523        if format == 'brackets':
524            suffix = '['
525            self.context.append(']')
526        else:
527            assert format == 'superscript'
528            suffix = '<sup>'
529            self.context.append('</sup>')
530        self.body.append(self.starttag(node, 'a', suffix,
531                                       CLASS='footnote-reference', href=href))
532
533    def depart_footnote_reference(self, node):
534        self.body.append(self.context.pop() + '</a>')
535
536    # just pass on generated text
537    def visit_generated(self, node):
538        pass
539
540    # Backwards-compatibility implementation:
541    # * Do not use <video>,
542    # * don't embed images,
543    # * use <object> instead of <img> for SVG.
544    #   (SVG not supported by IE up to version 8,
545    #   html4css1 strives for IE6 compatibility.)
546    object_image_types = {'.svg': 'image/svg+xml',
547                         '.swf': 'application/x-shockwave-flash'}
548    #
549    def visit_image(self, node):
550        atts = {}
551        uri = node['uri']
552        ext = os.path.splitext(uri)[1].lower()
553        if ext in self.object_image_types:
554            atts['data'] = uri
555            atts['type'] = self.object_image_types[ext]
556        else:
557            atts['src'] = uri
558            atts['alt'] = node.get('alt', uri)
559        # image size
560        if 'width' in node:
561            atts['width'] = node['width']
562        if 'height' in node:
563            atts['height'] = node['height']
564        if 'scale' in node:
565            if (PIL and not ('width' in node and 'height' in node)
566                and self.settings.file_insertion_enabled):
567                imagepath = url2pathname(uri)
568                try:
569                    img = PIL.Image.open(
570                            imagepath.encode(sys.getfilesystemencoding()))
571                except (IOError, UnicodeEncodeError):
572                    pass # TODO: warn?
573                else:
574                    self.settings.record_dependencies.add(
575                        imagepath.replace('\\', '/'))
576                    if 'width' not in atts:
577                        atts['width'] = '%dpx' % img.size[0]
578                    if 'height' not in atts:
579                        atts['height'] = '%dpx' % img.size[1]
580                    del img
581            for att_name in 'width', 'height':
582                if att_name in atts:
583                    match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
584                    assert match
585                    atts[att_name] = '%s%s' % (
586                        float(match.group(1)) * (float(node['scale']) / 100),
587                        match.group(2))
588        style = []
589        for att_name in 'width', 'height':
590            if att_name in atts:
591                if re.match(r'^[0-9.]+$', atts[att_name]):
592                    # Interpret unitless values as pixels.
593                    atts[att_name] += 'px'
594                style.append('%s: %s;' % (att_name, atts[att_name]))
595                del atts[att_name]
596        if style:
597            atts['style'] = ' '.join(style)
598        if (isinstance(node.parent, nodes.TextElement) or
599            (isinstance(node.parent, nodes.reference) and
600             not isinstance(node.parent.parent, nodes.TextElement))):
601            # Inline context or surrounded by <a>...</a>.
602            suffix = ''
603        else:
604            suffix = '\n'
605        if 'align' in node:
606            atts['class'] = 'align-%s' % node['align']
607        if ext in self.object_image_types:
608            # do NOT use an empty tag: incorrect rendering in browsers
609            self.body.append(self.starttag(node, 'object', '', **atts) +
610                             node.get('alt', uri) + '</object>' + suffix)
611        else:
612            self.body.append(self.emptytag(node, 'img', suffix, **atts))
613
614    def depart_image(self, node):
615        pass
616
617    # use table for footnote text,
618    # context added in footnote_backrefs.
619    def visit_label(self, node):
620        self.body.append(self.starttag(node, 'td', '%s[' % self.context.pop(),
621                                       CLASS='label'))
622
623    def depart_label(self, node):
624        self.body.append(']%s</td><td>%s' % (self.context.pop(), self.context.pop()))
625
626    # ersatz for first/last pseudo-classes
627    def visit_list_item(self, node):
628        self.body.append(self.starttag(node, 'li', ''))
629        if len(node):
630            node[0]['classes'].append('first')
631
632    # use <tt> (not supported by HTML5),
633    # cater for limited styling options in CSS1 using hard-coded NBSPs
634    def visit_literal(self, node):
635        # special case: "code" role
636        classes = node.get('classes', [])
637        if 'code' in classes:
638            # filter 'code' from class arguments
639            node['classes'] = [cls for cls in classes if cls != 'code']
640            self.body.append(self.starttag(node, 'code', ''))
641            return
642        self.body.append(
643            self.starttag(node, 'tt', '', CLASS='docutils literal'))
644        text = node.astext()
645        for token in self.words_and_spaces.findall(text):
646            if token.strip():
647                # Protect text like "--an-option" and the regular expression
648                # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
649                if self.in_word_wrap_point.search(token):
650                    self.body.append('<span class="pre">%s</span>'
651                                     % self.encode(token))
652                else:
653                    self.body.append(self.encode(token))
654            elif token in ('\n', ' '):
655                # Allow breaks at whitespace:
656                self.body.append(token)
657            else:
658                # Protect runs of multiple spaces; the last space can wrap:
659                self.body.append('&nbsp;' * (len(token) - 1) + ' ')
660        self.body.append('</tt>')
661        # Content already processed:
662        raise nodes.SkipNode
663
664    # add newline after opening tag, don't use <code> for code
665    def visit_literal_block(self, node):
666        self.body.append(self.starttag(node, 'pre', CLASS='literal-block'))
667
668    # add newline
669    def depart_literal_block(self, node):
670        self.body.append('\n</pre>\n')
671
672    # use table for option list
673    def visit_option_group(self, node):
674        atts = {}
675        if ( self.settings.option_limit
676             and len(node.astext()) > self.settings.option_limit):
677            atts['colspan'] = 2
678            self.context.append('</tr>\n<tr><td>&nbsp;</td>')
679        else:
680            self.context.append('')
681        self.body.append(
682            self.starttag(node, 'td', CLASS='option-group', **atts))
683        self.body.append('<kbd>')
684        self.context.append(0)          # count number of options
685
686    def depart_option_group(self, node):
687        self.context.pop()
688        self.body.append('</kbd></td>\n')
689        self.body.append(self.context.pop())
690
691    def visit_option_list(self, node):
692        self.body.append(
693              self.starttag(node, 'table', CLASS='docutils option-list',
694                            frame="void", rules="none"))
695        self.body.append('<col class="option" />\n'
696                         '<col class="description" />\n'
697                         '<tbody valign="top">\n')
698
699    def depart_option_list(self, node):
700        self.body.append('</tbody>\n</table>\n')
701
702    def visit_option_list_item(self, node):
703        self.body.append(self.starttag(node, 'tr', ''))
704
705    def depart_option_list_item(self, node):
706        self.body.append('</tr>\n')
707
708    # Omit <p> tags to produce visually compact lists (less vertical
709    # whitespace) as CSS styling requires CSS2.
710    def should_be_compact_paragraph(self, node):
711        """
712        Determine if the <p> tags around paragraph ``node`` can be omitted.
713        """
714        if (isinstance(node.parent, nodes.document) or
715            isinstance(node.parent, nodes.compound)):
716            # Never compact paragraphs in document or compound.
717            return False
718        for key, value in node.attlist():
719            if (node.is_not_default(key) and
720                not (key == 'classes' and value in
721                     ([], ['first'], ['last'], ['first', 'last']))):
722                # Attribute which needs to survive.
723                return False
724        first = isinstance(node.parent[0], nodes.label) # skip label
725        for child in node.parent.children[first:]:
726            # only first paragraph can be compact
727            if isinstance(child, nodes.Invisible):
728                continue
729            if child is node:
730                break
731            return False
732        parent_length = len([n for n in node.parent if not isinstance(
733            n, (nodes.Invisible, nodes.label))])
734        if ( self.compact_simple
735             or self.compact_field_list
736             or self.compact_p and parent_length == 1):
737            return True
738        return False
739
740    def visit_paragraph(self, node):
741        if self.should_be_compact_paragraph(node):
742            self.context.append('')
743        else:
744            self.body.append(self.starttag(node, 'p', ''))
745            self.context.append('</p>\n')
746
747    def depart_paragraph(self, node):
748        self.body.append(self.context.pop())
749
750    # ersatz for first/last pseudo-classes
751    def visit_sidebar(self, node):
752        self.body.append(
753            self.starttag(node, 'div', CLASS='sidebar'))
754        self.set_first_last(node)
755        self.in_sidebar = True
756
757    # <sub> not allowed in <pre>
758    def visit_subscript(self, node):
759        if isinstance(node.parent, nodes.literal_block):
760            self.body.append(self.starttag(node, 'span', '',
761                                           CLASS='subscript'))
762        else:
763            self.body.append(self.starttag(node, 'sub', ''))
764
765    def depart_subscript(self, node):
766        if isinstance(node.parent, nodes.literal_block):
767            self.body.append('</span>')
768        else:
769            self.body.append('</sub>')
770
771    # Use <h*> for subtitles (deprecated in HTML 5)
772    def visit_subtitle(self, node):
773        if isinstance(node.parent, nodes.sidebar):
774            self.body.append(self.starttag(node, 'p', '',
775                                           CLASS='sidebar-subtitle'))
776            self.context.append('</p>\n')
777        elif isinstance(node.parent, nodes.document):
778            self.body.append(self.starttag(node, 'h2', '', CLASS='subtitle'))
779            self.context.append('</h2>\n')
780            self.in_document_title = len(self.body)
781        elif isinstance(node.parent, nodes.section):
782            tag = 'h%s' % (self.section_level + self.initial_header_level - 1)
783            self.body.append(
784                self.starttag(node, tag, '', CLASS='section-subtitle') +
785                self.starttag({}, 'span', '', CLASS='section-subtitle'))
786            self.context.append('</span></%s>\n' % tag)
787
788    def depart_subtitle(self, node):
789        self.body.append(self.context.pop())
790        if self.in_document_title:
791            self.subtitle = self.body[self.in_document_title:-1]
792            self.in_document_title = 0
793            self.body_pre_docinfo.extend(self.body)
794            self.html_subtitle.extend(self.body)
795            del self.body[:]
796
797    # <sup> not allowed in <pre> in HTML 4
798    def visit_superscript(self, node):
799        if isinstance(node.parent, nodes.literal_block):
800            self.body.append(self.starttag(node, 'span', '',
801                                           CLASS='superscript'))
802        else:
803            self.body.append(self.starttag(node, 'sup', ''))
804
805    def depart_superscript(self, node):
806        if isinstance(node.parent, nodes.literal_block):
807            self.body.append('</span>')
808        else:
809            self.body.append('</sup>')
810
811    # <tt> element deprecated in HTML 5
812    def visit_system_message(self, node):
813        self.body.append(self.starttag(node, 'div', CLASS='system-message'))
814        self.body.append('<p class="system-message-title">')
815        backref_text = ''
816        if len(node['backrefs']):
817            backrefs = node['backrefs']
818            if len(backrefs) == 1:
819                backref_text = ('; <em><a href="#%s">backlink</a></em>'
820                                % backrefs[0])
821            else:
822                i = 1
823                backlinks = []
824                for backref in backrefs:
825                    backlinks.append('<a href="#%s">%s</a>' % (backref, i))
826                    i += 1
827                backref_text = ('; <em>backlinks: %s</em>'
828                                % ', '.join(backlinks))
829        if node.hasattr('line'):
830            line = ', line %s' % node['line']
831        else:
832            line = ''
833        self.body.append('System Message: %s/%s '
834                         '(<tt class="docutils">%s</tt>%s)%s</p>\n'
835                         % (node['type'], node['level'],
836                            self.encode(node['source']), line, backref_text))
837
838    # "hard coded" border setting
839    def visit_table(self, node):
840        self.context.append(self.compact_p)
841        self.compact_p = True
842        atts = {'border': 1}
843        classes = ['docutils', self.settings.table_style]
844        if 'align' in node:
845            classes.append('align-%s' % node['align'])
846        if 'width' in node:
847            atts['style'] = 'width: %s' % node['width']
848        self.body.append(
849            self.starttag(node, 'table', CLASS=' '.join(classes), **atts))
850
851    def depart_table(self, node):
852        self.compact_p = self.context.pop()
853        self.body.append('</table>\n')
854
855    # hard-coded vertical alignment
856    def visit_tbody(self, node):
857        self.body.append(self.starttag(node, 'tbody', valign='top'))
858    #
859    def depart_tbody(self, node):
860        self.body.append('</tbody>\n')
861
862    # hard-coded vertical alignment
863    def visit_thead(self, node):
864        self.body.append(self.starttag(node, 'thead', valign='bottom'))
865    #
866    def depart_thead(self, node):
867        self.body.append('</thead>\n')
868
869
870class SimpleListChecker(writers._html_base.SimpleListChecker):
871
872    """
873    Raise `nodes.NodeFound` if non-simple list item is encountered.
874
875    Here "simple" means a list item containing nothing other than a single
876    paragraph, a simple list, or a paragraph followed by a simple list.
877    """
878
879    def visit_list_item(self, node):
880        children = []
881        for child in node.children:
882            if not isinstance(child, nodes.Invisible):
883                children.append(child)
884        if (children and isinstance(children[0], nodes.paragraph)
885            and (isinstance(children[-1], nodes.bullet_list)
886                 or isinstance(children[-1], nodes.enumerated_list))):
887            children.pop()
888        if len(children) <= 1:
889            return
890        else:
891            raise nodes.NodeFound
892
893    # def visit_bullet_list(self, node):
894    #     pass
895
896    # def visit_enumerated_list(self, node):
897    #     pass
898
899    def visit_paragraph(self, node):
900        raise nodes.SkipNode
901
902    def visit_definition_list(self, node):
903        raise nodes.NodeFound
904
905    def visit_docinfo(self, node):
906        raise nodes.NodeFound
907