1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# :Author: David Goodger, Günter Milde
4#          Based on the html4css1 writer by David Goodger.
5# :Maintainer: docutils-develop@lists.sourceforge.net
6# :Revision: $Revision: 8644 $
7# :Date: $Date: 2005-06-28$
8# :Copyright: © 2016 David Goodger, Günter Milde
9# :License: Released under the terms of the `2-Clause BSD license`_, in short:
10#
11#    Copying and distribution of this file, with or without modification,
12#    are permitted in any medium without royalty provided the copyright
13#    notice and this notice are preserved.
14#    This file is offered as-is, without any warranty.
15#
16# .. _2-Clause BSD license: https://opensource.org/licenses/BSD-2-Clause
17
18"""common definitions for Docutils HTML writers"""
19
20import base64
21import mimetypes
22import os, os.path
23import re
24import sys
25
26try: # check for the Python Imaging Library
27    import PIL.Image
28except ImportError:
29    try:  # sometimes PIL modules are put in PYTHONPATH's root
30        import Image
31        class PIL(object): pass  # dummy wrapper
32        PIL.Image = Image
33    except ImportError:
34        PIL = None
35
36import docutils
37from docutils import nodes, utils, writers, languages, io
38from docutils.utils.error_reporting import SafeString
39from docutils.transforms import writer_aux
40from docutils.utils.math import (unichar2tex, pick_math_environment,
41                                 math2html, latex2mathml, tex2mathml_extern)
42
43if sys.version_info >= (3, 0):
44    from urllib.request import url2pathname
45else:
46    from urllib import url2pathname
47
48if sys.version_info >= (3, 0):
49    unicode = str  # noqa
50
51
52class Writer(writers.Writer):
53
54    supported = ('html', 'xhtml') # update in subclass
55    """Formats this writer supports."""
56
57    # default_stylesheets = [] # set in subclass!
58    # default_stylesheet_dirs = ['.'] # set in subclass!
59    default_template = 'template.txt'
60    # default_template_path = ... # set in subclass!
61    # settings_spec = ... # set in subclass!
62
63    settings_defaults = {'output_encoding_error_handler': 'xmlcharrefreplace'}
64
65    # config_section = ... # set in subclass!
66    config_section_dependencies = ('writers', 'html writers')
67
68    visitor_attributes = (
69        'head_prefix', 'head', 'stylesheet', 'body_prefix',
70        'body_pre_docinfo', 'docinfo', 'body', 'body_suffix',
71        'title', 'subtitle', 'header', 'footer', 'meta', 'fragment',
72        'html_prolog', 'html_head', 'html_title', 'html_subtitle',
73        'html_body')
74
75    def get_transforms(self):
76        return writers.Writer.get_transforms(self) + [writer_aux.Admonitions]
77
78    def translate(self):
79        self.visitor = visitor = self.translator_class(self.document)
80        self.document.walkabout(visitor)
81        for attr in self.visitor_attributes:
82            setattr(self, attr, getattr(visitor, attr))
83        self.output = self.apply_template()
84
85    def apply_template(self):
86        template_file = open(self.document.settings.template, 'rb')
87        template = unicode(template_file.read(), 'utf-8')
88        template_file.close()
89        subs = self.interpolation_dict()
90        return template % subs
91
92    def interpolation_dict(self):
93        subs = {}
94        settings = self.document.settings
95        for attr in self.visitor_attributes:
96            subs[attr] = ''.join(getattr(self, attr)).rstrip('\n')
97        subs['encoding'] = settings.output_encoding
98        subs['version'] = docutils.__version__
99        return subs
100
101    def assemble_parts(self):
102        writers.Writer.assemble_parts(self)
103        for part in self.visitor_attributes:
104            self.parts[part] = ''.join(getattr(self, part))
105
106
107class HTMLTranslator(nodes.NodeVisitor):
108
109    """
110    Generic Docutils to HTML translator.
111
112    See the `html4css1` and `html5_polyglot` writers for full featured
113    HTML writers.
114
115    .. IMPORTANT::
116      The `visit_*` and `depart_*` methods use a
117      heterogeneous stack, `self.context`.
118      When subclassing, make sure to be consistent in its use!
119
120      Examples for robust coding:
121
122      a) Override both `visit_*` and `depart_*` methods, don't call the
123         parent functions.
124
125      b) Extend both and unconditionally call the parent functions::
126
127           def visit_example(self, node):
128               if foo:
129                   self.body.append('<div class="foo">')
130               html4css1.HTMLTranslator.visit_example(self, node)
131
132           def depart_example(self, node):
133               html4css1.HTMLTranslator.depart_example(self, node)
134               if foo:
135                   self.body.append('</div>')
136
137      c) Extend both, calling the parent functions under the same
138         conditions::
139
140           def visit_example(self, node):
141               if foo:
142                   self.body.append('<div class="foo">\n')
143               else: # call the parent method
144                   _html_base.HTMLTranslator.visit_example(self, node)
145
146           def depart_example(self, node):
147               if foo:
148                   self.body.append('</div>\n')
149               else: # call the parent method
150                   _html_base.HTMLTranslator.depart_example(self, node)
151
152      d) Extend one method (call the parent), but don't otherwise use the
153         `self.context` stack::
154
155           def depart_example(self, node):
156               _html_base.HTMLTranslator.depart_example(self, node)
157               if foo:
158                   # implementation-specific code
159                   # that does not use `self.context`
160                   self.body.append('</div>\n')
161
162      This way, changes in stack use will not bite you.
163    """
164
165    xml_declaration = '<?xml version="1.0" encoding="%s" ?>\n'
166    doctype = '<!DOCTYPE html>\n'
167    doctype_mathml = doctype
168
169    head_prefix_template = ('<html xmlns="http://www.w3.org/1999/xhtml"'
170                            ' xml:lang="%(lang)s" lang="%(lang)s">\n<head>\n')
171    content_type = '<meta charset="%s"/>\n'
172    generator = ('<meta name="generator" content="Docutils %s: '
173                 'http://docutils.sourceforge.net/" />\n')
174
175    # Template for the MathJax script in the header:
176    mathjax_script = '<script type="text/javascript" src="%s"></script>\n'
177
178    mathjax_url = 'file:/usr/share/javascript/mathjax/MathJax.js'
179    """
180    URL of the MathJax javascript library.
181
182    The MathJax library ought to be installed on the same
183    server as the rest of the deployed site files and specified
184    in the `math-output` setting appended to "mathjax".
185    See `Docutils Configuration`__.
186
187    __ http://docutils.sourceforge.net/docs/user/config.html#math-output
188
189    The fallback tries a local MathJax installation at
190    ``/usr/share/javascript/mathjax/MathJax.js``.
191    """
192
193    stylesheet_link = '<link rel="stylesheet" href="%s" type="text/css" />\n'
194    embedded_stylesheet = '<style type="text/css">\n\n%s\n</style>\n'
195    words_and_spaces = re.compile(r'[^ \n]+| +|\n')
196    # wrap point inside word:
197    in_word_wrap_point = re.compile(r'.+\W\W.+|[-?].+', re.U)
198    lang_attribute = 'lang' # name changes to 'xml:lang' in XHTML 1.1
199
200    special_characters = {ord('&'): u'&amp;',
201                          ord('<'): u'&lt;',
202                          ord('"'): u'&quot;',
203                          ord('>'): u'&gt;',
204                          ord('@'): u'&#64;', # may thwart address harvesters
205                         }
206    """Character references for characters with a special meaning in HTML."""
207
208
209    def __init__(self, document):
210        nodes.NodeVisitor.__init__(self, document)
211        self.settings = settings = document.settings
212        lcode = settings.language_code
213        self.language = languages.get_language(lcode, document.reporter)
214        self.meta = [self.generator % docutils.__version__]
215        self.head_prefix = []
216        self.html_prolog = []
217        if settings.xml_declaration:
218            self.head_prefix.append(self.xml_declaration
219                                    % settings.output_encoding)
220            # self.content_type = ""
221            # encoding not interpolated:
222            self.html_prolog.append(self.xml_declaration)
223        self.head = self.meta[:]
224        self.stylesheet = [self.stylesheet_call(path)
225                           for path in utils.get_stylesheet_list(settings)]
226        self.body_prefix = ['</head>\n<body>\n']
227        # document title, subtitle display
228        self.body_pre_docinfo = []
229        # author, date, etc.
230        self.docinfo = []
231        self.body = []
232        self.fragment = []
233        self.body_suffix = ['</body>\n</html>\n']
234        self.section_level = 0
235        self.initial_header_level = int(settings.initial_header_level)
236
237        self.math_output = settings.math_output.split()
238        self.math_output_options = self.math_output[1:]
239        self.math_output = self.math_output[0].lower()
240
241        self.context = []
242        """Heterogeneous stack.
243
244        Used by visit_* and depart_* functions in conjunction with the tree
245        traversal. Make sure that the pops correspond to the pushes."""
246
247        self.topic_classes = []
248        self.colspecs = []
249        self.compact_p = True
250        self.compact_simple = False
251        self.compact_field_list = False
252        self.in_docinfo = False
253        self.in_sidebar = False
254        self.in_footnote_list = False
255        self.title = []
256        self.subtitle = []
257        self.header = []
258        self.footer = []
259        self.html_head = [self.content_type] # charset not interpolated
260        self.html_title = []
261        self.html_subtitle = []
262        self.html_body = []
263        self.in_document_title = 0   # len(self.body) or 0
264        self.in_mailto = False
265        self.author_in_authors = False # for html4css1
266        self.math_header = []
267
268    def astext(self):
269        return ''.join(self.head_prefix + self.head
270                       + self.stylesheet + self.body_prefix
271                       + self.body_pre_docinfo + self.docinfo
272                       + self.body + self.body_suffix)
273
274    def encode(self, text):
275        """Encode special characters in `text` & return."""
276        # Use only named entities known in both XML and HTML
277        # other characters are automatically encoded "by number" if required.
278        # @@@ A codec to do these and all other HTML entities would be nice.
279        text = unicode(text)
280        return text.translate(self.special_characters)
281
282    def cloak_mailto(self, uri):
283        """Try to hide a mailto: URL from harvesters."""
284        # Encode "@" using a URL octet reference (see RFC 1738).
285        # Further cloaking with HTML entities will be done in the
286        # `attval` function.
287        return uri.replace('@', '%40')
288
289    def cloak_email(self, addr):
290        """Try to hide the link text of a email link from harversters."""
291        # Surround at-signs and periods with <span> tags.  ("@" has
292        # already been encoded to "&#64;" by the `encode` method.)
293        addr = addr.replace('&#64;', '<span>&#64;</span>')
294        addr = addr.replace('.', '<span>&#46;</span>')
295        return addr
296
297    def attval(self, text,
298               whitespace=re.compile('[\n\r\t\v\f]')):
299        """Cleanse, HTML encode, and return attribute value text."""
300        encoded = self.encode(whitespace.sub(' ', text))
301        if self.in_mailto and self.settings.cloak_email_addresses:
302            # Cloak at-signs ("%40") and periods with HTML entities.
303            encoded = encoded.replace('%40', '&#37;&#52;&#48;')
304            encoded = encoded.replace('.', '&#46;')
305        return encoded
306
307    def stylesheet_call(self, path):
308        """Return code to reference or embed stylesheet file `path`"""
309        if self.settings.embed_stylesheet:
310            try:
311                content = io.FileInput(source_path=path,
312                                       encoding='utf-8').read()
313                self.settings.record_dependencies.add(path)
314            except IOError as err:
315                msg = u"Cannot embed stylesheet '%r': %s." % (
316                                path, SafeString(err.strerror))
317                self.document.reporter.error(msg)
318                return '<--- %s --->\n' % msg
319            return self.embedded_stylesheet % content
320        # else link to style file:
321        if self.settings.stylesheet_path:
322            # adapt path relative to output (cf. config.html#stylesheet-path)
323            path = utils.relative_path(self.settings._destination, path)
324        return self.stylesheet_link % self.encode(path)
325
326    def starttag(self, node, tagname, suffix='\n', empty=False, **attributes):
327        """
328        Construct and return a start tag given a node (id & class attributes
329        are extracted), tag name, and optional attributes.
330        """
331        tagname = tagname.lower()
332        prefix = []
333        atts = {}
334        ids = []
335        for (name, value) in attributes.items():
336            atts[name.lower()] = value
337        classes = []
338        languages = []
339        # unify class arguments and move language specification
340        for cls in node.get('classes', []) + atts.pop('class', '').split():
341            if cls.startswith('language-'):
342                languages.append(cls[9:])
343            elif cls.strip() and cls not in classes:
344                classes.append(cls)
345        if languages:
346            # attribute name is 'lang' in XHTML 1.0 but 'xml:lang' in 1.1
347            atts[self.lang_attribute] = languages[0]
348        if classes:
349            atts['class'] = ' '.join(classes)
350        assert 'id' not in atts
351        ids.extend(node.get('ids', []))
352        if 'ids' in atts:
353            ids.extend(atts['ids'])
354            del atts['ids']
355        if ids:
356            atts['id'] = ids[0]
357            for id in ids[1:]:
358                # Add empty "span" elements for additional IDs.  Note
359                # that we cannot use empty "a" elements because there
360                # may be targets inside of references, but nested "a"
361                # elements aren't allowed in XHTML (even if they do
362                # not all have a "href" attribute).
363                if empty or isinstance(node,
364                            (nodes.bullet_list, nodes.docinfo,
365                             nodes.definition_list, nodes.enumerated_list,
366                             nodes.field_list, nodes.option_list,
367                             nodes.table)):
368                    # Insert target right in front of element.
369                    prefix.append('<span id="%s"></span>' % id)
370                else:
371                    # Non-empty tag.  Place the auxiliary <span> tag
372                    # *inside* the element, as the first child.
373                    suffix += '<span id="%s"></span>' % id
374        attlist = sorted(atts.items())
375        parts = [tagname]
376        for name, value in attlist:
377            # value=None was used for boolean attributes without
378            # value, but this isn't supported by XHTML.
379            assert value is not None
380            if isinstance(value, list):
381                values = [unicode(v) for v in value]
382                parts.append('%s="%s"' % (name.lower(),
383                                          self.attval(' '.join(values))))
384            else:
385                parts.append('%s="%s"' % (name.lower(),
386                                          self.attval(unicode(value))))
387        if empty:
388            infix = ' /'
389        else:
390            infix = ''
391        return ''.join(prefix) + '<%s%s>' % (' '.join(parts), infix) + suffix
392
393    def emptytag(self, node, tagname, suffix='\n', **attributes):
394        """Construct and return an XML-compatible empty tag."""
395        return self.starttag(node, tagname, suffix, empty=True, **attributes)
396
397    def set_class_on_child(self, node, class_, index=0):
398        """
399        Set class `class_` on the visible child no. index of `node`.
400        Do nothing if node has fewer children than `index`.
401        """
402        children = [n for n in node if not isinstance(n, nodes.Invisible)]
403        try:
404            child = children[index]
405        except IndexError:
406            return
407        child['classes'].append(class_)
408
409    def visit_Text(self, node):
410        text = node.astext()
411        encoded = self.encode(text)
412        if self.in_mailto and self.settings.cloak_email_addresses:
413            encoded = self.cloak_email(encoded)
414        self.body.append(encoded)
415
416    def depart_Text(self, node):
417        pass
418
419    def visit_abbreviation(self, node):
420        # @@@ implementation incomplete ("title" attribute)
421        self.body.append(self.starttag(node, 'abbr', ''))
422
423    def depart_abbreviation(self, node):
424        self.body.append('</abbr>')
425
426    def visit_acronym(self, node):
427        # @@@ implementation incomplete ("title" attribute)
428        self.body.append(self.starttag(node, 'acronym', ''))
429
430    def depart_acronym(self, node):
431        self.body.append('</acronym>')
432
433    def visit_address(self, node):
434        self.visit_docinfo_item(node, 'address', meta=False)
435        self.body.append(self.starttag(node, 'pre',
436                                       suffix= '', CLASS='address'))
437
438    def depart_address(self, node):
439        self.body.append('\n</pre>\n')
440        self.depart_docinfo_item()
441
442    def visit_admonition(self, node):
443        node['classes'].insert(0, 'admonition')
444        self.body.append(self.starttag(node, 'div'))
445
446    def depart_admonition(self, node=None):
447        self.body.append('</div>\n')
448
449    attribution_formats = {'dash': (u'\u2014', ''),
450                           'parentheses': ('(', ')'),
451                           'parens': ('(', ')'),
452                           'none': ('', '')}
453
454    def visit_attribution(self, node):
455        prefix, suffix = self.attribution_formats[self.settings.attribution]
456        self.context.append(suffix)
457        self.body.append(
458            self.starttag(node, 'p', prefix, CLASS='attribution'))
459
460    def depart_attribution(self, node):
461        self.body.append(self.context.pop() + '</p>\n')
462
463    def visit_author(self, node):
464        if not(isinstance(node.parent, nodes.authors)):
465            self.visit_docinfo_item(node, 'author')
466        self.body.append('<p>')
467
468    def depart_author(self, node):
469        self.body.append('</p>')
470        if isinstance(node.parent, nodes.authors):
471            self.body.append('\n')
472        else:
473            self.depart_docinfo_item()
474
475    def visit_authors(self, node):
476        self.visit_docinfo_item(node, 'authors')
477
478    def depart_authors(self, node):
479        self.depart_docinfo_item()
480
481    def visit_block_quote(self, node):
482        self.body.append(self.starttag(node, 'blockquote'))
483
484    def depart_block_quote(self, node):
485        self.body.append('</blockquote>\n')
486
487    def check_simple_list(self, node):
488        """Check for a simple list that can be rendered compactly."""
489        visitor = SimpleListChecker(self.document)
490        try:
491            node.walk(visitor)
492        except nodes.NodeFound:
493            return False
494        else:
495            return True
496
497    # Compact lists
498    # ------------
499    # Include definition lists and field lists (in addition to ordered
500    # and unordered lists) in the test if a list is "simple"  (cf. the
501    # html4css1.HTMLTranslator docstring and the SimpleListChecker class at
502    # the end of this file).
503
504    def is_compactable(self, node):
505        # explicite class arguments have precedence
506        if 'compact' in node['classes']:
507            return True
508        if 'open' in node['classes']:
509            return False
510        # check config setting:
511        if (isinstance(node, (nodes.field_list, nodes.definition_list))
512            and not self.settings.compact_field_lists):
513            return False
514        if (isinstance(node, (nodes.enumerated_list, nodes.bullet_list))
515            and not self.settings.compact_lists):
516            return False
517        # Table of Contents:
518        if (self.topic_classes == ['contents']):
519            # TODO: look in parent nodes, remove self.topic_classes?
520            return True
521        # check the list items:
522        return self.check_simple_list(node)
523
524    def visit_bullet_list(self, node):
525        atts = {}
526        old_compact_simple = self.compact_simple
527        self.context.append((self.compact_simple, self.compact_p))
528        self.compact_p = None
529        self.compact_simple = self.is_compactable(node)
530        if self.compact_simple and not old_compact_simple:
531            atts['class'] = 'simple'
532        self.body.append(self.starttag(node, 'ul', **atts))
533
534    def depart_bullet_list(self, node):
535        self.compact_simple, self.compact_p = self.context.pop()
536        self.body.append('</ul>\n')
537
538    def visit_caption(self, node):
539        self.body.append(self.starttag(node, 'p', '', CLASS='caption'))
540
541    def depart_caption(self, node):
542        self.body.append('</p>\n')
543
544    def visit_citation(self, node):
545        # Use definition list for bibliographic references.
546        # Join adjacent citation entries.
547        # TODO: use <aside>.
548        if not self.in_footnote_list:
549            listnode = node.copy()
550            listnode['ids'] = []
551            self.body.append(self.starttag(listnode, 'dl', CLASS='citation'))
552            # self.body.append('<dl class="citation">\n')
553            self.in_footnote_list = True
554
555    def depart_citation(self, node):
556        self.body.append('</dd>\n')
557        if not isinstance(node.next_node(descend=False, siblings=True),
558                          nodes.citation):
559            self.body.append('</dl>\n')
560            self.in_footnote_list = False
561
562    def visit_citation_reference(self, node):
563        href = '#'
564        if 'refid' in node:
565            href += node['refid']
566        elif 'refname' in node:
567            href += self.document.nameids[node['refname']]
568        # else: # TODO system message (or already in the transform)?
569        # 'Citation reference missing.'
570        self.body.append(self.starttag(
571            node, 'a', '[', CLASS='citation-reference', href=href))
572
573    def depart_citation_reference(self, node):
574        self.body.append(']</a>')
575
576     # classifier
577    # ----------
578    # don't insert classifier-delimiter here (done by CSS)
579
580    def visit_classifier(self, node):
581        self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
582
583    def depart_classifier(self, node):
584        self.body.append('</span>')
585
586    def visit_colspec(self, node):
587        self.colspecs.append(node)
588        # "stubs" list is an attribute of the tgroup element:
589        node.parent.stubs.append(node.attributes.get('stub'))
590
591    def depart_colspec(self, node):
592        # write out <colgroup> when all colspecs are processed
593        if isinstance(node.next_node(descend=False, siblings=True),
594                      nodes.colspec):
595            return
596        if 'colwidths-auto' in node.parent.parent['classes'] or (
597            'colwidths-auto' in self.settings.table_style and
598            ('colwidths-given' not in node.parent.parent['classes'])):
599            return
600        total_width = sum(node['colwidth'] for node in self.colspecs)
601        self.body.append(self.starttag(node, 'colgroup'))
602        for node in self.colspecs:
603            colwidth = int(node['colwidth'] * 100.0 / total_width + 0.5)
604            self.body.append(self.emptytag(node, 'col',
605                                           style='width: %i%%' % colwidth))
606        self.body.append('</colgroup>\n')
607
608    def visit_comment(self, node,
609                      sub=re.compile('-(?=-)').sub):
610        """Escape double-dashes in comment text."""
611        self.body.append('<!-- %s -->\n' % sub('- ', node.astext()))
612        # Content already processed:
613        raise nodes.SkipNode
614
615    def visit_compound(self, node):
616        self.body.append(self.starttag(node, 'div', CLASS='compound'))
617        if len(node) > 1:
618            node[0]['classes'].append('compound-first')
619            node[-1]['classes'].append('compound-last')
620            for child in node[1:-1]:
621                child['classes'].append('compound-middle')
622
623    def depart_compound(self, node):
624        self.body.append('</div>\n')
625
626    def visit_container(self, node):
627        self.body.append(self.starttag(node, 'div', CLASS='docutils container'))
628
629    def depart_container(self, node):
630        self.body.append('</div>\n')
631
632    def visit_contact(self, node):
633        self.visit_docinfo_item(node, 'contact', meta=False)
634
635    def depart_contact(self, node):
636        self.depart_docinfo_item()
637
638    def visit_copyright(self, node):
639        self.visit_docinfo_item(node, 'copyright')
640
641    def depart_copyright(self, node):
642        self.depart_docinfo_item()
643
644    def visit_date(self, node):
645        self.visit_docinfo_item(node, 'date')
646
647    def depart_date(self, node):
648        self.depart_docinfo_item()
649
650    def visit_decoration(self, node):
651        pass
652
653    def depart_decoration(self, node):
654        pass
655
656    def visit_definition(self, node):
657        self.body.append('</dt>\n')
658        self.body.append(self.starttag(node, 'dd', ''))
659
660    def depart_definition(self, node):
661        self.body.append('</dd>\n')
662
663    def visit_definition_list(self, node):
664        classes = node.setdefault('classes', [])
665        if self.is_compactable(node):
666            classes.append('simple')
667        self.body.append(self.starttag(node, 'dl'))
668
669    def depart_definition_list(self, node):
670        self.body.append('</dl>\n')
671
672    def visit_definition_list_item(self, node):
673        # pass class arguments, ids and names to definition term:
674        node.children[0]['classes'] = (
675            node.get('classes', []) + node.children[0].get('classes', []))
676        node.children[0]['ids'] = (
677            node.get('ids', []) + node.children[0].get('ids', []))
678        node.children[0]['names'] = (
679            node.get('names', []) + node.children[0].get('names', []))
680
681    def depart_definition_list_item(self, node):
682        pass
683
684    def visit_description(self, node):
685        self.body.append(self.starttag(node, 'dd', ''))
686
687    def depart_description(self, node):
688        self.body.append('</dd>\n')
689
690    def visit_docinfo(self, node):
691        self.context.append(len(self.body))
692        classes = 'docinfo'
693        if (self.is_compactable(node)):
694            classes += ' simple'
695        self.body.append(self.starttag(node, 'dl', CLASS=classes))
696
697    def depart_docinfo(self, node):
698        self.body.append('</dl>\n')
699        start = self.context.pop()
700        self.docinfo = self.body[start:]
701        self.body = []
702
703    def visit_docinfo_item(self, node, name, meta=True):
704        if meta:
705            meta_tag = '<meta name="%s" content="%s" />\n' \
706                       % (name, self.attval(node.astext()))
707            self.add_meta(meta_tag)
708        self.body.append('<dt class="%s">%s</dt>\n'
709                         % (name, self.language.labels[name]))
710        self.body.append(self.starttag(node, 'dd', '', CLASS=name))
711
712    def depart_docinfo_item(self):
713        self.body.append('</dd>\n')
714
715    def visit_doctest_block(self, node):
716        self.body.append(self.starttag(node, 'pre', suffix='',
717                                       CLASS='code python doctest'))
718
719    def depart_doctest_block(self, node):
720        self.body.append('\n</pre>\n')
721
722    def visit_document(self, node):
723        title = (node.get('title', '') or os.path.basename(node['source'])
724                 or 'docutils document without title')
725        self.head.append('<title>%s</title>\n' % self.encode(title))
726
727    def depart_document(self, node):
728        self.head_prefix.extend([self.doctype,
729                                 self.head_prefix_template %
730                                 {'lang': self.settings.language_code}])
731        self.html_prolog.append(self.doctype)
732        self.meta.insert(0, self.content_type % self.settings.output_encoding)
733        self.head.insert(0, self.content_type % self.settings.output_encoding)
734        if 'name="dcterms.' in ''.join(self.meta):
735            self.head.append(
736             '<link rel="schema.dcterms" href="http://purl.org/dc/terms/"/>')
737        if self.math_header:
738            if self.math_output == 'mathjax':
739                self.head.extend(self.math_header)
740            else:
741                self.stylesheet.extend(self.math_header)
742        # skip content-type meta tag with interpolated charset value:
743        self.html_head.extend(self.head[1:])
744        self.body_prefix.append(self.starttag(node, 'div', CLASS='document'))
745        self.body_suffix.insert(0, '</div>\n')
746        self.fragment.extend(self.body) # self.fragment is the "naked" body
747        self.html_body.extend(self.body_prefix[1:] + self.body_pre_docinfo
748                              + self.docinfo + self.body
749                              + self.body_suffix[:-1])
750        assert not self.context, 'len(context) = %s' % len(self.context)
751
752    def visit_emphasis(self, node):
753        self.body.append(self.starttag(node, 'em', ''))
754
755    def depart_emphasis(self, node):
756        self.body.append('</em>')
757
758    def visit_entry(self, node):
759        atts = {'class': []}
760        if isinstance(node.parent.parent, nodes.thead):
761            atts['class'].append('head')
762        if node.parent.parent.parent.stubs[node.parent.column]:
763            # "stubs" list is an attribute of the tgroup element
764            atts['class'].append('stub')
765        if atts['class']:
766            tagname = 'th'
767            atts['class'] = ' '.join(atts['class'])
768        else:
769            tagname = 'td'
770            del atts['class']
771        node.parent.column += 1
772        if 'morerows' in node:
773            atts['rowspan'] = node['morerows'] + 1
774        if 'morecols' in node:
775            atts['colspan'] = node['morecols'] + 1
776            node.parent.column += node['morecols']
777        self.body.append(self.starttag(node, tagname, '', **atts))
778        self.context.append('</%s>\n' % tagname.lower())
779        # TODO: why does the html4css1 writer insert an NBSP into empty cells?
780        # if len(node) == 0:              # empty cell
781        #     self.body.append('&#0160;') # no-break space
782
783    def depart_entry(self, node):
784        self.body.append(self.context.pop())
785
786    def visit_enumerated_list(self, node):
787        atts = {}
788        if 'start' in node:
789            atts['start'] = node['start']
790        if 'enumtype' in node:
791            atts['class'] = node['enumtype']
792        if self.is_compactable(node):
793            atts['class'] = (atts.get('class', '') + ' simple').strip()
794        self.body.append(self.starttag(node, 'ol', **atts))
795
796    def depart_enumerated_list(self, node):
797        self.body.append('</ol>\n')
798
799    def visit_field_list(self, node):
800        # Keep simple paragraphs in the field_body to enable CSS
801        # rule to start body on new line if the label is too long
802        classes = 'field-list'
803        if (self.is_compactable(node)):
804            classes += ' simple'
805        self.body.append(self.starttag(node, 'dl', CLASS=classes))
806
807    def depart_field_list(self, node):
808        self.body.append('</dl>\n')
809
810    def visit_field(self, node):
811        pass
812
813    def depart_field(self, node):
814        pass
815
816    # as field is ignored, pass class arguments to field-name and field-body:
817
818    def visit_field_name(self, node):
819        self.body.append(self.starttag(node, 'dt', '',
820                                       CLASS=''.join(node.parent['classes'])))
821
822    def depart_field_name(self, node):
823        self.body.append('</dt>\n')
824
825    def visit_field_body(self, node):
826        self.body.append(self.starttag(node, 'dd', '',
827                                       CLASS=''.join(node.parent['classes'])))
828        # prevent misalignment of following content if the field is empty:
829        if not node.children:
830            self.body.append('<p></p>')
831
832    def depart_field_body(self, node):
833        self.body.append('</dd>\n')
834
835    def visit_figure(self, node):
836        atts = {'class': 'figure'}
837        if node.get('width'):
838            atts['style'] = 'width: %s' % node['width']
839        if node.get('align'):
840            atts['class'] += " align-" + node['align']
841        self.body.append(self.starttag(node, 'div', **atts))
842
843    def depart_figure(self, node):
844        self.body.append('</div>\n')
845
846    # use HTML 5 <footer> element?
847    def visit_footer(self, node):
848        self.context.append(len(self.body))
849
850    def depart_footer(self, node):
851        start = self.context.pop()
852        footer = [self.starttag(node, 'div', CLASS='footer'),
853                  '<hr class="footer" />\n']
854        footer.extend(self.body[start:])
855        footer.append('\n</div>\n')
856        self.footer.extend(footer)
857        self.body_suffix[:0] = footer
858        del self.body[start:]
859
860    # TODO: use the new HTML5 element <aside> for footnote text
861    # (allows better styling with CSS, the current <dl> list styling
862    # with "float" interferes with sidebars).
863    def visit_footnote(self, node):
864        if not self.in_footnote_list:
865            listnode = node.copy()
866            listnode['ids'] = []
867            classes = 'footnote ' + self.settings.footnote_references
868            self.body.append(self.starttag(listnode, 'dl', CLASS=classes))
869            # self.body.append('<dl class="%s">\n'%classes)
870            self.in_footnote_list = True
871
872    def depart_footnote(self, node):
873        self.body.append('</dd>\n')
874        if not isinstance(node.next_node(descend=False, siblings=True),
875                          nodes.footnote):
876            self.body.append('</dl>\n')
877            self.in_footnote_list = False
878
879    def visit_footnote_reference(self, node):
880        href = '#' + node['refid']
881        classes = 'footnote-reference ' + self.settings.footnote_references
882        self.body.append(self.starttag(node, 'a', '', #suffix,
883                                       CLASS=classes, href=href))
884
885    def depart_footnote_reference(self, node):
886        self.body.append('</a>')
887
888    # Docutils-generated text: put section numbers in a span for CSS styling:
889    def visit_generated(self, node):
890        if 'sectnum' in node['classes']:
891            # get section number (strip trailing no-break-spaces)
892            sectnum = node.astext().rstrip(u' ')
893            self.body.append('<span class="sectnum">%s</span> '
894                                    % self.encode(sectnum))
895            # Content already processed:
896            raise nodes.SkipNode
897
898    def depart_generated(self, node):
899        pass
900
901    def visit_header(self, node):
902        self.context.append(len(self.body))
903
904    def depart_header(self, node):
905        start = self.context.pop()
906        header = [self.starttag(node, 'div', CLASS='header')]
907        header.extend(self.body[start:])
908        header.append('\n<hr class="header"/>\n</div>\n')
909        self.body_prefix.extend(header)
910        self.header.extend(header)
911        del self.body[start:]
912
913    def visit_image(self, node):
914        atts = {}
915        uri = node['uri']
916        mimetype = mimetypes.guess_type(uri)[0]
917        # image size
918        if 'width' in node:
919            atts['width'] = node['width']
920        if 'height' in node:
921            atts['height'] = node['height']
922        if 'scale' in node:
923            if (PIL and not ('width' in node and 'height' in node)
924                and self.settings.file_insertion_enabled):
925                imagepath = url2pathname(uri)
926                try:
927                    img = PIL.Image.open(
928                            imagepath.encode(sys.getfilesystemencoding()))
929                except (IOError, UnicodeEncodeError):
930                    pass # TODO: warn?
931                else:
932                    self.settings.record_dependencies.add(
933                        imagepath.replace('\\', '/'))
934                    if 'width' not in atts:
935                        atts['width'] = '%dpx' % img.size[0]
936                    if 'height' not in atts:
937                        atts['height'] = '%dpx' % img.size[1]
938                    del img
939            for att_name in 'width', 'height':
940                if att_name in atts:
941                    match = re.match(r'([0-9.]+)(\S*)$', atts[att_name])
942                    assert match
943                    atts[att_name] = '%s%s' % (
944                        float(match.group(1)) * (float(node['scale']) / 100),
945                        match.group(2))
946        style = []
947        for att_name in 'width', 'height':
948            if att_name in atts:
949                if re.match(r'^[0-9.]+$', atts[att_name]):
950                    # Interpret unitless values as pixels.
951                    atts[att_name] += 'px'
952                style.append('%s: %s;' % (att_name, atts[att_name]))
953                del atts[att_name]
954        if style:
955            atts['style'] = ' '.join(style)
956        if (isinstance(node.parent, nodes.TextElement) or
957            (isinstance(node.parent, nodes.reference) and
958             not isinstance(node.parent.parent, nodes.TextElement))):
959            # Inline context or surrounded by <a>...</a>.
960            suffix = ''
961        else:
962            suffix = '\n'
963        if 'align' in node:
964            atts['class'] = 'align-%s' % node['align']
965        # Embed image file (embedded SVG or data URI):
966        if self.settings.embed_images or ('embed' in node):
967            err_msg = ''
968            if not mimetype:
969                err_msg = 'unknown MIME type'
970            if not self.settings.file_insertion_enabled:
971                err_msg = 'file insertion disabled.'
972            try:
973                with open(url2pathname(uri), 'rb') as imagefile:
974                    imagedata = imagefile.read()
975            except IOError as err:
976                err_msg = err.strerror
977            if err_msg:
978                self.document.reporter.error('Cannot embed image %r: %s'
979                                             %(uri, err_msg))
980            else:
981                self.settings.record_dependencies.add(
982                                            uri.replace('\\', '/'))
983                # TODO: insert SVG as-is?
984                # if mimetype == 'image/svg+xml':
985                  # read/parse, apply arguments,
986                  # insert as <svg ....> ... </svg> # (about 1/3 less data)
987                data64 = base64.b64encode(imagedata).decode()
988                uri = u'data:%s;base64,%s' % (mimetype, data64)
989        if mimetype == 'application/x-shockwave-flash':
990            atts['type'] = mimetype
991            # do NOT use an empty tag: incorrect rendering in browsers
992            tag = (self.starttag(node, 'object', '', data=uri, **atts)
993                   + node.get('alt', uri) + '</object>' + suffix)
994        else:
995            atts['alt'] = node.get('alt', node['uri'])
996            tag = self.emptytag(node, 'img', suffix, src=uri, **atts)
997        self.body.append(tag)
998
999    def depart_image(self, node):
1000        pass
1001
1002    def visit_inline(self, node):
1003        self.body.append(self.starttag(node, 'span', ''))
1004
1005    def depart_inline(self, node):
1006        self.body.append('</span>')
1007
1008    # footnote and citation labels:
1009    def visit_label(self, node):
1010        if (isinstance(node.parent, nodes.footnote)):
1011            classes = self.settings.footnote_references
1012        else:
1013            classes = 'brackets'
1014        # pass parent node to get id into starttag:
1015        self.body.append(self.starttag(node.parent, 'dt', '', CLASS='label'))
1016        self.body.append(self.starttag(node, 'span', '', CLASS=classes))
1017        # footnote/citation backrefs:
1018        if self.settings.footnote_backlinks:
1019            backrefs = node.parent['backrefs']
1020            if len(backrefs) == 1:
1021                self.body.append('<a class="fn-backref" href="#%s">'
1022                                 % backrefs[0])
1023
1024    def depart_label(self, node):
1025        if self.settings.footnote_backlinks:
1026            backrefs = node.parent['backrefs']
1027            if len(backrefs) == 1:
1028                self.body.append('</a>')
1029        self.body.append('</span>')
1030        if self.settings.footnote_backlinks and len(backrefs) > 1:
1031            backlinks = ['<a href="#%s">%s</a>' % (ref, i)
1032                            for (i, ref) in enumerate(backrefs, 1)]
1033            self.body.append('<span class="fn-backref">(%s)</span>'
1034                                % ','.join(backlinks))
1035        self.body.append('</dt>\n<dd>')
1036
1037    def visit_legend(self, node):
1038        self.body.append(self.starttag(node, 'div', CLASS='legend'))
1039
1040    def depart_legend(self, node):
1041        self.body.append('</div>\n')
1042
1043    def visit_line(self, node):
1044        self.body.append(self.starttag(node, 'div', suffix='', CLASS='line'))
1045        if not len(node):
1046            self.body.append('<br />')
1047
1048    def depart_line(self, node):
1049        self.body.append('</div>\n')
1050
1051    def visit_line_block(self, node):
1052        self.body.append(self.starttag(node, 'div', CLASS='line-block'))
1053
1054    def depart_line_block(self, node):
1055        self.body.append('</div>\n')
1056
1057    def visit_list_item(self, node):
1058        self.body.append(self.starttag(node, 'li', ''))
1059
1060    def depart_list_item(self, node):
1061        self.body.append('</li>\n')
1062
1063    # inline literal
1064    def visit_literal(self, node):
1065        # special case: "code" role
1066        classes = node.get('classes', [])
1067        if 'code' in classes:
1068            # filter 'code' from class arguments
1069            node['classes'] = [cls for cls in classes if cls != 'code']
1070            self.body.append(self.starttag(node, 'code', ''))
1071            return
1072        self.body.append(
1073            self.starttag(node, 'span', '', CLASS='docutils literal'))
1074        text = node.astext()
1075        # remove hard line breaks (except if in a parsed-literal block)
1076        if not isinstance(node.parent, nodes.literal_block):
1077            text = text.replace('\n', ' ')
1078        # Protect text like ``--an-option`` and the regular expression
1079        # ``[+]?(\d+(\.\d*)?|\.\d+)`` from bad line wrapping
1080        for token in self.words_and_spaces.findall(text):
1081            if token.strip() and self.in_word_wrap_point.search(token):
1082                self.body.append('<span class="pre">%s</span>'
1083                                    % self.encode(token))
1084            else:
1085                self.body.append(self.encode(token))
1086        self.body.append('</span>')
1087        # Content already processed:
1088        raise nodes.SkipNode
1089
1090    def depart_literal(self, node):
1091        # skipped unless literal element is from "code" role:
1092        self.body.append('</code>')
1093
1094    def visit_literal_block(self, node):
1095        self.body.append(self.starttag(node, 'pre', '', CLASS='literal-block'))
1096        if 'code' in node.get('classes', []):
1097            self.body.append('<code>')
1098
1099    def depart_literal_block(self, node):
1100        if 'code' in node.get('classes', []):
1101            self.body.append('</code>')
1102        self.body.append('</pre>\n')
1103
1104    # Mathematics:
1105    # As there is no native HTML math support, we provide alternatives
1106    # for the math-output: LaTeX and MathJax simply wrap the content,
1107    # HTML and MathML also convert the math_code.
1108    # HTML container
1109    math_tags = {# math_output: (block, inline, class-arguments)
1110                 'mathml':      ('div', '', ''),
1111                 'html':        ('div', 'span', 'formula'),
1112                 'mathjax':     ('div', 'span', 'math'),
1113                 'latex':       ('pre', 'tt',   'math'),
1114                }
1115
1116    def visit_math(self, node, math_env=''):
1117        # If the method is called from visit_math_block(), math_env != ''.
1118
1119        if self.math_output not in self.math_tags:
1120            self.document.reporter.error(
1121                'math-output format "%s" not supported '
1122                'falling back to "latex"'% self.math_output)
1123            self.math_output = 'latex'
1124        tag = self.math_tags[self.math_output][math_env == '']
1125        clsarg = self.math_tags[self.math_output][2]
1126        # LaTeX container
1127        wrappers = {# math_mode: (inline, block)
1128                    'mathml':  ('$%s$',   u'\\begin{%s}\n%s\n\\end{%s}'),
1129                    'html':    ('$%s$',   u'\\begin{%s}\n%s\n\\end{%s}'),
1130                    'mathjax': (r'\(%s\)', u'\\begin{%s}\n%s\n\\end{%s}'),
1131                    'latex':   (None,     None),
1132                   }
1133        wrapper = wrappers[self.math_output][math_env != '']
1134        if self.math_output == 'mathml' and (not self.math_output_options or
1135                                self.math_output_options[0] == 'blahtexml'):
1136            wrapper = None
1137        # get and wrap content
1138        math_code = node.astext().translate(unichar2tex.uni2tex_table)
1139        if wrapper:
1140            try: # wrapper with three "%s"
1141                math_code = wrapper % (math_env, math_code, math_env)
1142            except TypeError: # wrapper with one "%s"
1143                math_code = wrapper % math_code
1144        # settings and conversion
1145        if self.math_output in ('latex', 'mathjax'):
1146            math_code = self.encode(math_code)
1147        if self.math_output == 'mathjax' and not self.math_header:
1148            try:
1149                self.mathjax_url = self.math_output_options[0]
1150            except IndexError:
1151                self.document.reporter.warning('No MathJax URL specified, '
1152                    'using local fallback (see config.html)')
1153            # append configuration, if not already present in the URL:
1154            # input LaTeX with AMS, output common HTML
1155            if '?' not in self.mathjax_url:
1156                self.mathjax_url += '?config=TeX-AMS_CHTML'
1157            self.math_header = [self.mathjax_script % self.mathjax_url]
1158        elif self.math_output == 'html':
1159            if self.math_output_options and not self.math_header:
1160                self.math_header = [self.stylesheet_call(
1161                    utils.find_file_in_dirs(s, self.settings.stylesheet_dirs))
1162                    for s in self.math_output_options[0].split(',')]
1163            # TODO: fix display mode in matrices and fractions
1164            math2html.DocumentParameters.displaymode = (math_env != '')
1165            math_code = math2html.math2html(math_code)
1166        elif self.math_output == 'mathml':
1167            if  'XHTML 1' in self.doctype:
1168                self.doctype = self.doctype_mathml
1169                self.content_type = self.content_type_mathml
1170            converter = ' '.join(self.math_output_options).lower()
1171            try:
1172                if converter == 'latexml':
1173                    math_code = tex2mathml_extern.latexml(math_code,
1174                                                    self.document.reporter)
1175                elif converter == 'ttm':
1176                    math_code = tex2mathml_extern.ttm(math_code,
1177                                                    self.document.reporter)
1178                elif converter == 'blahtexml':
1179                    math_code = tex2mathml_extern.blahtexml(math_code,
1180                        inline=not(math_env),
1181                        reporter=self.document.reporter)
1182                elif not converter:
1183                    math_code = latex2mathml.tex2mathml(math_code,
1184                                                        inline=not(math_env))
1185                else:
1186                    self.document.reporter.error('option "%s" not supported '
1187                    'with math-output "MathML"')
1188            except OSError:
1189                    raise OSError('is "latexmlmath" in your PATH?')
1190            except SyntaxError as err:
1191                err_node = self.document.reporter.error(err, base_node=node)
1192                self.visit_system_message(err_node)
1193                self.body.append(self.starttag(node, 'p'))
1194                self.body.append(u','.join(err.args))
1195                self.body.append('</p>\n')
1196                self.body.append(self.starttag(node, 'pre',
1197                                               CLASS='literal-block'))
1198                self.body.append(self.encode(math_code))
1199                self.body.append('\n</pre>\n')
1200                self.depart_system_message(err_node)
1201                raise nodes.SkipNode
1202        # append to document body
1203        if tag:
1204            self.body.append(self.starttag(node, tag,
1205                                           suffix='\n'*bool(math_env),
1206                                           CLASS=clsarg))
1207        self.body.append(math_code)
1208        if math_env: # block mode (equation, display)
1209            self.body.append('\n')
1210        if tag:
1211            self.body.append('</%s>' % tag)
1212        if math_env:
1213            self.body.append('\n')
1214        # Content already processed:
1215        raise nodes.SkipNode
1216
1217    def depart_math(self, node):
1218        pass # never reached
1219
1220    def visit_math_block(self, node):
1221        math_env = pick_math_environment(node.astext())
1222        self.visit_math(node, math_env=math_env)
1223
1224    def depart_math_block(self, node):
1225        pass # never reached
1226
1227    # Meta tags: 'lang' attribute replaced by 'xml:lang' in XHTML 1.1
1228    # HTML5/polyglot recommends using both
1229    def visit_meta(self, node):
1230        meta = self.emptytag(node, 'meta', **node.non_default_attributes())
1231        self.add_meta(meta)
1232
1233    def depart_meta(self, node):
1234        pass
1235
1236    def add_meta(self, tag):
1237        self.meta.append(tag)
1238        self.head.append(tag)
1239
1240    def visit_option(self, node):
1241        self.body.append(self.starttag(node, 'span', '', CLASS='option'))
1242
1243    def depart_option(self, node):
1244        self.body.append('</span>')
1245        if isinstance(node.next_node(descend=False, siblings=True),
1246                      nodes.option):
1247            self.body.append(', ')
1248
1249    def visit_option_argument(self, node):
1250        self.body.append(node.get('delimiter', ' '))
1251        self.body.append(self.starttag(node, 'var', ''))
1252
1253    def depart_option_argument(self, node):
1254        self.body.append('</var>')
1255
1256    def visit_option_group(self, node):
1257        self.body.append(self.starttag(node, 'dt', ''))
1258        self.body.append('<kbd>')
1259
1260    def depart_option_group(self, node):
1261        self.body.append('</kbd></dt>\n')
1262
1263    def visit_option_list(self, node):
1264        self.body.append(
1265            self.starttag(node, 'dl', CLASS='option-list'))
1266
1267    def depart_option_list(self, node):
1268        self.body.append('</dl>\n')
1269
1270    def visit_option_list_item(self, node):
1271        pass
1272
1273    def depart_option_list_item(self, node):
1274        pass
1275
1276    def visit_option_string(self, node):
1277        pass
1278
1279    def depart_option_string(self, node):
1280        pass
1281
1282    def visit_organization(self, node):
1283        self.visit_docinfo_item(node, 'organization')
1284
1285    def depart_organization(self, node):
1286        self.depart_docinfo_item()
1287
1288    # Do not omit <p> tags
1289    # --------------------
1290    #
1291    # The HTML4CSS1 writer does this to "produce
1292    # visually compact lists (less vertical whitespace)". This writer
1293    # relies on CSS rules for"visual compactness".
1294    #
1295    # * In XHTML 1.1, e.g. a <blockquote> element may not contain
1296    #   character data, so you cannot drop the <p> tags.
1297    # * Keeping simple paragraphs in the field_body enables a CSS
1298    #   rule to start the field-body on a new line if the label is too long
1299    # * it makes the code simpler.
1300    #
1301    # TODO: omit paragraph tags in simple table cells?
1302
1303    def visit_paragraph(self, node):
1304        self.body.append(self.starttag(node, 'p', ''))
1305
1306    def depart_paragraph(self, node):
1307        self.body.append('</p>')
1308        if not (isinstance(node.parent, (nodes.list_item, nodes.entry)) and
1309                (len(node.parent) == 1)):
1310            self.body.append('\n')
1311
1312    def visit_problematic(self, node):
1313        if node.hasattr('refid'):
1314            self.body.append('<a href="#%s">' % node['refid'])
1315            self.context.append('</a>')
1316        else:
1317            self.context.append('')
1318        self.body.append(self.starttag(node, 'span', '', CLASS='problematic'))
1319
1320    def depart_problematic(self, node):
1321        self.body.append('</span>')
1322        self.body.append(self.context.pop())
1323
1324    def visit_raw(self, node):
1325        if 'html' in node.get('format', '').split():
1326            t = isinstance(node.parent, nodes.TextElement) and 'span' or 'div'
1327            if node['classes']:
1328                self.body.append(self.starttag(node, t, suffix=''))
1329            self.body.append(node.astext())
1330            if node['classes']:
1331                self.body.append('</%s>' % t)
1332        # Keep non-HTML raw text out of output:
1333        raise nodes.SkipNode
1334
1335    def visit_reference(self, node):
1336        atts = {'class': 'reference'}
1337        if 'refuri' in node:
1338            atts['href'] = node['refuri']
1339            if ( self.settings.cloak_email_addresses
1340                 and atts['href'].startswith('mailto:')):
1341                atts['href'] = self.cloak_mailto(atts['href'])
1342                self.in_mailto = True
1343            atts['class'] += ' external'
1344        else:
1345            assert 'refid' in node, \
1346                   'References must have "refuri" or "refid" attribute.'
1347            atts['href'] = '#' + node['refid']
1348            atts['class'] += ' internal'
1349        if len(node) == 1 and isinstance(node[0], nodes.image):
1350            atts['class'] += ' image-reference'
1351        if not isinstance(node.parent, nodes.TextElement):
1352            assert len(node) == 1 and isinstance(node[0], nodes.image)
1353            atts['class'] += ' image-reference'
1354        self.body.append(self.starttag(node, 'a', '', **atts))
1355
1356    def depart_reference(self, node):
1357        self.body.append('</a>')
1358        if not isinstance(node.parent, nodes.TextElement):
1359            self.body.append('\n')
1360        self.in_mailto = False
1361
1362    def visit_revision(self, node):
1363        self.visit_docinfo_item(node, 'revision', meta=False)
1364
1365    def depart_revision(self, node):
1366        self.depart_docinfo_item()
1367
1368    def visit_row(self, node):
1369        self.body.append(self.starttag(node, 'tr', ''))
1370        node.column = 0
1371
1372    def depart_row(self, node):
1373        self.body.append('</tr>\n')
1374
1375    def visit_rubric(self, node):
1376        self.body.append(self.starttag(node, 'p', '', CLASS='rubric'))
1377
1378    def depart_rubric(self, node):
1379        self.body.append('</p>\n')
1380
1381    def visit_section(self, node):
1382        self.section_level += 1
1383        self.body.append(
1384            self.starttag(node, 'div', CLASS='section'))
1385
1386    def depart_section(self, node):
1387        self.section_level -= 1
1388        self.body.append('</div>\n')
1389
1390    # TODO: use the new HTML5 element <aside>
1391    def visit_sidebar(self, node):
1392        self.body.append(
1393            self.starttag(node, 'div', CLASS='sidebar'))
1394        self.in_sidebar = True
1395
1396    def depart_sidebar(self, node):
1397        self.body.append('</div>\n')
1398        self.in_sidebar = False
1399
1400    def visit_status(self, node):
1401        self.visit_docinfo_item(node, 'status', meta=False)
1402
1403    def depart_status(self, node):
1404        self.depart_docinfo_item()
1405
1406    def visit_strong(self, node):
1407        self.body.append(self.starttag(node, 'strong', ''))
1408
1409    def depart_strong(self, node):
1410        self.body.append('</strong>')
1411
1412    def visit_subscript(self, node):
1413        self.body.append(self.starttag(node, 'sub', ''))
1414
1415    def depart_subscript(self, node):
1416        self.body.append('</sub>')
1417
1418    def visit_substitution_definition(self, node):
1419        """Internal only."""
1420        raise nodes.SkipNode
1421
1422    def visit_substitution_reference(self, node):
1423        self.unimplemented_visit(node)
1424
1425    # h1–h6 elements must not be used to markup subheadings, subtitles,
1426    # alternative titles and taglines unless intended to be the heading for a
1427    # new section or subsection.
1428    # -- http://www.w3.org/TR/html/sections.html#headings-and-sections
1429    def visit_subtitle(self, node):
1430        if isinstance(node.parent, nodes.sidebar):
1431            classes = 'sidebar-subtitle'
1432        elif isinstance(node.parent, nodes.document):
1433            classes = 'subtitle'
1434            self.in_document_title = len(self.body)+1
1435        elif isinstance(node.parent, nodes.section):
1436            classes = 'section-subtitle'
1437        self.body.append(self.starttag(node, 'p', '', CLASS=classes))
1438
1439    def depart_subtitle(self, node):
1440        self.body.append('</p>\n')
1441        if isinstance(node.parent, nodes.document):
1442            self.subtitle = self.body[self.in_document_title:-1]
1443            self.in_document_title = 0
1444            self.body_pre_docinfo.extend(self.body)
1445            self.html_subtitle.extend(self.body)
1446            del self.body[:]
1447
1448    def visit_superscript(self, node):
1449        self.body.append(self.starttag(node, 'sup', ''))
1450
1451    def depart_superscript(self, node):
1452        self.body.append('</sup>')
1453
1454    def visit_system_message(self, node):
1455        self.body.append(self.starttag(node, 'div', CLASS='system-message'))
1456        self.body.append('<p class="system-message-title">')
1457        backref_text = ''
1458        if len(node['backrefs']):
1459            backrefs = node['backrefs']
1460            if len(backrefs) == 1:
1461                backref_text = ('; <em><a href="#%s">backlink</a></em>'
1462                                % backrefs[0])
1463            else:
1464                i = 1
1465                backlinks = []
1466                for backref in backrefs:
1467                    backlinks.append('<a href="#%s">%s</a>' % (backref, i))
1468                    i += 1
1469                backref_text = ('; <em>backlinks: %s</em>'
1470                                % ', '.join(backlinks))
1471        if node.hasattr('line'):
1472            line = ', line %s' % node['line']
1473        else:
1474            line = ''
1475        self.body.append('System Message: %s/%s '
1476                         '(<span class="docutils literal">%s</span>%s)%s</p>\n'
1477                         % (node['type'], node['level'],
1478                            self.encode(node['source']), line, backref_text))
1479
1480    def depart_system_message(self, node):
1481        self.body.append('</div>\n')
1482
1483    def visit_table(self, node):
1484        atts = {}
1485        classes = [cls.strip(u' \t\n')
1486                   for cls in self.settings.table_style.split(',')]
1487        if 'align' in node:
1488            classes.append('align-%s' % node['align'])
1489        if 'width' in node:
1490            atts['style'] = 'width: %s' % node['width']
1491        tag = self.starttag(node, 'table', CLASS=' '.join(classes), **atts)
1492        self.body.append(tag)
1493
1494    def depart_table(self, node):
1495        self.body.append('</table>\n')
1496
1497    def visit_target(self, node):
1498        if not ('refuri' in node or 'refid' in node
1499                or 'refname' in node):
1500            self.body.append(self.starttag(node, 'span', '', CLASS='target'))
1501            self.context.append('</span>')
1502        else:
1503            self.context.append('')
1504
1505    def depart_target(self, node):
1506        self.body.append(self.context.pop())
1507
1508    # no hard-coded vertical alignment in table body
1509    def visit_tbody(self, node):
1510        self.body.append(self.starttag(node, 'tbody'))
1511
1512    def depart_tbody(self, node):
1513        self.body.append('</tbody>\n')
1514
1515    def visit_term(self, node):
1516        self.body.append(self.starttag(node, 'dt', ''))
1517
1518    def depart_term(self, node):
1519        """
1520        Leave the end tag to `self.visit_definition()`, in case there's a
1521        classifier.
1522        """
1523        pass
1524
1525    def visit_tgroup(self, node):
1526        self.colspecs = []
1527        node.stubs = []
1528
1529    def depart_tgroup(self, node):
1530        pass
1531
1532    def visit_thead(self, node):
1533        self.body.append(self.starttag(node, 'thead'))
1534
1535    def depart_thead(self, node):
1536        self.body.append('</thead>\n')
1537
1538    def visit_title(self, node):
1539        """Only 6 section levels are supported by HTML."""
1540        close_tag = '</p>\n'
1541        if isinstance(node.parent, nodes.topic):
1542            self.body.append(
1543                  self.starttag(node, 'p', '', CLASS='topic-title'))
1544        elif isinstance(node.parent, nodes.sidebar):
1545            self.body.append(
1546                  self.starttag(node, 'p', '', CLASS='sidebar-title'))
1547        elif isinstance(node.parent, nodes.Admonition):
1548            self.body.append(
1549                  self.starttag(node, 'p', '', CLASS='admonition-title'))
1550        elif isinstance(node.parent, nodes.table):
1551            self.body.append(
1552                  self.starttag(node, 'caption', ''))
1553            close_tag = '</caption>\n'
1554        elif isinstance(node.parent, nodes.document):
1555            self.body.append(self.starttag(node, 'h1', '', CLASS='title'))
1556            close_tag = '</h1>\n'
1557            self.in_document_title = len(self.body)
1558        else:
1559            assert isinstance(node.parent, nodes.section)
1560            h_level = self.section_level + self.initial_header_level - 1
1561            atts = {}
1562            if (len(node.parent) >= 2 and
1563                isinstance(node.parent[1], nodes.subtitle)):
1564                atts['CLASS'] = 'with-subtitle'
1565            self.body.append(
1566                  self.starttag(node, 'h%s' % h_level, '', **atts))
1567            atts = {}
1568            if node.hasattr('refid'):
1569                atts['class'] = 'toc-backref'
1570                atts['href'] = '#' + node['refid']
1571            if atts:
1572                self.body.append(self.starttag({}, 'a', '', **atts))
1573                close_tag = '</a></h%s>\n' % (h_level)
1574            else:
1575                close_tag = '</h%s>\n' % (h_level)
1576        self.context.append(close_tag)
1577
1578    def depart_title(self, node):
1579        self.body.append(self.context.pop())
1580        if self.in_document_title:
1581            self.title = self.body[self.in_document_title:-1]
1582            self.in_document_title = 0
1583            self.body_pre_docinfo.extend(self.body)
1584            self.html_title.extend(self.body)
1585            del self.body[:]
1586
1587    def visit_title_reference(self, node):
1588        self.body.append(self.starttag(node, 'cite', ''))
1589
1590    def depart_title_reference(self, node):
1591        self.body.append('</cite>')
1592
1593    def visit_topic(self, node):
1594        self.body.append(self.starttag(node, 'div', CLASS='topic'))
1595        self.topic_classes = node['classes']
1596
1597    def depart_topic(self, node):
1598        self.body.append('</div>\n')
1599        self.topic_classes = []
1600
1601    def visit_transition(self, node):
1602        self.body.append(self.emptytag(node, 'hr', CLASS='docutils'))
1603
1604    def depart_transition(self, node):
1605        pass
1606
1607    def visit_version(self, node):
1608        self.visit_docinfo_item(node, 'version', meta=False)
1609
1610    def depart_version(self, node):
1611        self.depart_docinfo_item()
1612
1613    def unimplemented_visit(self, node):
1614        raise NotImplementedError('visiting unimplemented node type: %s'
1615                                  % node.__class__.__name__)
1616
1617
1618class SimpleListChecker(nodes.GenericNodeVisitor):
1619
1620    """
1621    Raise `nodes.NodeFound` if non-simple list item is encountered.
1622
1623    Here "simple" means a list item containing nothing other than a single
1624    paragraph, a simple list, or a paragraph followed by a simple list.
1625
1626    This version also checks for simple field lists and docinfo.
1627    """
1628
1629    def default_visit(self, node):
1630        raise nodes.NodeFound
1631
1632    def visit_list_item(self, node):
1633        children = [child for child in node.children
1634                    if not isinstance(child, nodes.Invisible)]
1635        if (children and isinstance(children[0], nodes.paragraph)
1636            and (isinstance(children[-1], nodes.bullet_list) or
1637                 isinstance(children[-1], nodes.enumerated_list) or
1638                 isinstance(children[-1], nodes.field_list))):
1639            children.pop()
1640        if len(children) <= 1:
1641            return
1642        else:
1643            raise nodes.NodeFound
1644
1645    def pass_node(self, node):
1646        pass
1647
1648    def ignore_node(self, node):
1649        # ignore nodes that are never complex (can contain only inline nodes)
1650        raise nodes.SkipNode
1651
1652    # Paragraphs and text
1653    visit_Text = ignore_node
1654    visit_paragraph = ignore_node
1655
1656    # Lists
1657    visit_bullet_list = pass_node
1658    visit_enumerated_list = pass_node
1659    visit_docinfo = pass_node
1660
1661    # Docinfo nodes:
1662    visit_author = ignore_node
1663    visit_authors = visit_list_item
1664    visit_address = visit_list_item
1665    visit_contact = pass_node
1666    visit_copyright = ignore_node
1667    visit_date = ignore_node
1668    visit_organization = ignore_node
1669    visit_status = ignore_node
1670    visit_version = visit_list_item
1671
1672    # Definition list:
1673    visit_definition_list = pass_node
1674    visit_definition_list_item = pass_node
1675    visit_term = ignore_node
1676    visit_classifier = pass_node
1677    visit_definition = visit_list_item
1678
1679    # Field list:
1680    visit_field_list = pass_node
1681    visit_field = pass_node
1682    # the field body corresponds to a list item
1683    visit_field_body = visit_list_item
1684    visit_field_name = ignore_node
1685
1686    # Invisible nodes should be ignored.
1687    visit_comment = ignore_node
1688    visit_substitution_definition = ignore_node
1689    visit_target = ignore_node
1690    visit_pending = ignore_node
1691