"""docutils writers handling Sphinx' custom nodes."""

import os
import posixpath
import re
import urllib.parse
import warnings
from typing import TYPE_CHECKING, Iterable, Optional, Tuple, cast

from docutils import nodes
from docutils.nodes import Element, Node, Text
from docutils.writers.html4css1 import HTMLTranslator as BaseTranslator
from docutils.writers.html4css1 import Writer

from sphinx import addnodes
from sphinx.builders import Builder
from sphinx.deprecation import RemovedInSphinx60Warning
from sphinx.locale import _, __, admonitionlabels
from sphinx.util import logging
from sphinx.util.docutils import SphinxTranslator
from sphinx.util.images import get_image_size

if TYPE_CHECKING:
    from sphinx.builders.html import StandaloneHTMLBuilder


logger = logging.getLogger(__name__)

# A good overview of the purpose behind these classes can be found here:
# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html


def multiply_length(length: str, scale: int) -> str:
    """Multiply *length* (width or height) by *scale*."""
    matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length)
    if not matched:
        return length
    elif scale == 100:
        return length
    else:
        amount, unit = matched.groups()
        result = float(amount) * scale / 100
        return "%s%s" % (int(result), unit)


class HTMLWriter(Writer):

    # override embed-stylesheet default value to False.
    settings_default_overrides = {"embed_stylesheet": False}

    def __init__(self, builder: "StandaloneHTMLBuilder") -> None:
        super().__init__()
        self.builder = builder

    def translate(self) -> None:
        # sadly, this is mostly copied from parent class
        visitor = self.builder.create_translator(self.document, self.builder)
        self.visitor = cast(HTMLTranslator, visitor)
        self.document.walkabout(visitor)
        self.output = self.visitor.astext()
        for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix',
                     'body_pre_docinfo', 'docinfo', 'body', 'fragment',
                     'body_suffix', 'meta', 'title', 'subtitle', 'header',
                     'footer', 'html_prolog', 'html_head', 'html_title',
                     'html_subtitle', 'html_body', ):
            setattr(self, attr, getattr(visitor, attr, None))
        self.clean_meta = ''.join(self.visitor.meta[2:])


# RemovedInSphinx70Warning
class HTMLTranslator(SphinxTranslator, BaseTranslator):
    """
    Our custom HTML translator.
    """

    builder: "StandaloneHTMLBuilder"

    def __init__(self, document: nodes.document, builder: Builder) -> None:
        super().__init__(document, builder)

        self.highlighter = self.builder.highlighter
        self.docnames = [self.builder.current_docname]  # for singlehtml builder
        self.manpages_url = self.config.manpages_url
        self.protect_literal_text = 0
        self.secnumber_suffix = self.config.html_secnumber_suffix
        self.param_separator = ''
        self.optional_param_level = 0
        self._table_row_indices = [0]
        self._fieldlist_row_indices = [0]
        self.required_params_left = 0

    def visit_start_of_file(self, node: Element) -> None:
        # only occurs in the single-file builder
        self.docnames.append(node['docname'])
        self.body.append('<span id="document-%s"></span>' % node['docname'])

    def depart_start_of_file(self, node: Element) -> None:
        self.docnames.pop()

    #############################################################
    # Domain-specific object descriptions
    #############################################################

    # Top-level nodes for descriptions
    ##################################

    def visit_desc(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'dl'))

    def depart_desc(self, node: Element) -> None:
        self.body.append('</dl>\n\n')

    def visit_desc_signature(self, node: Element) -> None:
        # the id is set automatically
        self.body.append(self.starttag(node, 'dt'))
        self.protect_literal_text += 1

    def depart_desc_signature(self, node: Element) -> None:
        self.protect_literal_text -= 1
        if not node.get('is_multiline'):
            self.add_permalink_ref(node, _('Permalink to this definition'))
        self.body.append('</dt>\n')

    def visit_desc_signature_line(self, node: Element) -> None:
        pass

    def depart_desc_signature_line(self, node: Element) -> None:
        if node.get('add_permalink'):
            # the permalink info is on the parent desc_signature node
            self.add_permalink_ref(node.parent, _('Permalink to this definition'))
        self.body.append('<br />')

    def visit_desc_content(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'dd', ''))

    def depart_desc_content(self, node: Element) -> None:
        self.body.append('</dd>')

    def visit_desc_inline(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'span', ''))

    def depart_desc_inline(self, node: Element) -> None:
        self.body.append('</span>')

    # Nodes for high-level structure in signatures
    ##############################################

    def visit_desc_name(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'code', ''))

    def depart_desc_name(self, node: Element) -> None:
        self.body.append('</code>')

    def visit_desc_addname(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'code', ''))

    def depart_desc_addname(self, node: Element) -> None:
        self.body.append('</code>')

    def visit_desc_type(self, node: Element) -> None:
        pass

    def depart_desc_type(self, node: Element) -> None:
        pass

    def visit_desc_returns(self, node: Element) -> None:
        self.body.append(' <span class="sig-return">')
        self.body.append('<span class="sig-return-icon">&#x2192;</span>')
        self.body.append(' <span class="sig-return-typehint">')

    def depart_desc_returns(self, node: Element) -> None:
        self.body.append('</span></span>')

    def visit_desc_parameterlist(self, node: Element) -> None:
        self.body.append('<span class="sig-paren">(</span>')
        self.first_param = 1
        self.optional_param_level = 0
        # How many required parameters are left.
        self.required_params_left = sum([isinstance(c, addnodes.desc_parameter)
                                         for c in node.children])
        self.param_separator = node.child_text_separator

    def depart_desc_parameterlist(self, node: Element) -> None:
        self.body.append('<span class="sig-paren">)</span>')

    # If required parameters are still to come, then put the comma after
    # the parameter.  Otherwise, put the comma before.  This ensures that
    # signatures like the following render correctly (see issue #1001):
    #
    #     foo([a, ]b, c[, d])
    #
    def visit_desc_parameter(self, node: Element) -> None:
        if self.first_param:
            self.first_param = 0
        elif not self.required_params_left:
            self.body.append(self.param_separator)
        if self.optional_param_level == 0:
            self.required_params_left -= 1
        if not node.hasattr('noemph'):
            self.body.append('<em>')

    def depart_desc_parameter(self, node: Element) -> None:
        if not node.hasattr('noemph'):
            self.body.append('</em>')
        if self.required_params_left:
            self.body.append(self.param_separator)

    def visit_desc_optional(self, node: Element) -> None:
        self.optional_param_level += 1
        self.body.append('<span class="optional">[</span>')

    def depart_desc_optional(self, node: Element) -> None:
        self.optional_param_level -= 1
        self.body.append('<span class="optional">]</span>')

    def visit_desc_annotation(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'em', '', CLASS='property'))

    def depart_desc_annotation(self, node: Element) -> None:
        self.body.append('</em>')

    ##############################################

    def visit_versionmodified(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'div', CLASS=node['type']))

    def depart_versionmodified(self, node: Element) -> None:
        self.body.append('</div>\n')

    # overwritten
    def visit_reference(self, node: Element) -> None:
        atts = {'class': 'reference'}
        if node.get('internal') or 'refuri' not in node:
            atts['class'] += ' internal'
        else:
            atts['class'] += ' external'
        if 'refuri' in node:
            atts['href'] = node['refuri'] or '#'
            if self.settings.cloak_email_addresses and atts['href'].startswith('mailto:'):
                atts['href'] = self.cloak_mailto(atts['href'])
                self.in_mailto = True
        else:
            assert 'refid' in node, \
                   'References must have "refuri" or "refid" attribute.'
            atts['href'] = '#' + node['refid']
        if not isinstance(node.parent, nodes.TextElement):
            assert len(node) == 1 and isinstance(node[0], nodes.image)
            atts['class'] += ' image-reference'
        if 'reftitle' in node:
            atts['title'] = node['reftitle']
        if 'target' in node:
            atts['target'] = node['target']
        self.body.append(self.starttag(node, 'a', '', **atts))

        if node.get('secnumber'):
            self.body.append(('%s' + self.secnumber_suffix) %
                             '.'.join(map(str, node['secnumber'])))

    def visit_number_reference(self, node: Element) -> None:
        self.visit_reference(node)

    def depart_number_reference(self, node: Element) -> None:
        self.depart_reference(node)

    # overwritten -- we don't want source comments to show up in the HTML
    def visit_comment(self, node: Element) -> None:  # type: ignore
        raise nodes.SkipNode

    # overwritten
    def visit_admonition(self, node: Element, name: str = '') -> None:
        self.body.append(self.starttag(
            node, 'div', CLASS=('admonition ' + name)))
        if name:
            node.insert(0, nodes.title(name, admonitionlabels[name]))
        self.set_first_last(node)

    def depart_admonition(self, node: Optional[Element] = None) -> None:
        self.body.append('</div>\n')

    def visit_seealso(self, node: Element) -> None:
        self.visit_admonition(node, 'seealso')

    def depart_seealso(self, node: Element) -> None:
        self.depart_admonition(node)

    def get_secnumber(self, node: Element) -> Optional[Tuple[int, ...]]:
        if node.get('secnumber'):
            return node['secnumber']
        elif isinstance(node.parent, nodes.section):
            if self.builder.name == 'singlehtml':
                docname = self.docnames[-1]
                anchorname = "%s/#%s" % (docname, node.parent['ids'][0])
                if anchorname not in self.builder.secnumbers:
                    anchorname = "%s/" % docname  # try first heading which has no anchor
            else:
                anchorname = '#' + node.parent['ids'][0]
                if anchorname not in self.builder.secnumbers:
                    anchorname = ''  # try first heading which has no anchor

            if self.builder.secnumbers.get(anchorname):
                return self.builder.secnumbers[anchorname]

        return None

    def add_secnumber(self, node: Element) -> None:
        secnumber = self.get_secnumber(node)
        if secnumber:
            self.body.append('<span class="section-number">%s</span>' %
                             ('.'.join(map(str, secnumber)) + self.secnumber_suffix))

    def add_fignumber(self, node: Element) -> None:
        def append_fignumber(figtype: str, figure_id: str) -> None:
            if self.builder.name == 'singlehtml':
                key = "%s/%s" % (self.docnames[-1], figtype)
            else:
                key = figtype

            if figure_id in self.builder.fignumbers.get(key, {}):
                self.body.append('<span class="caption-number">')
                prefix = self.config.numfig_format.get(figtype)
                if prefix is None:
                    msg = __('numfig_format is not defined for %s') % figtype
                    logger.warning(msg)
                else:
                    numbers = self.builder.fignumbers[key][figure_id]
                    self.body.append(prefix % '.'.join(map(str, numbers)) + ' ')
                    self.body.append('</span>')

        figtype = self.builder.env.domains['std'].get_enumerable_node_type(node)
        if figtype:
            if len(node['ids']) == 0:
                msg = __('Any IDs not assigned for %s node') % node.tagname
                logger.warning(msg, location=node)
            else:
                append_fignumber(figtype, node['ids'][0])

    def add_permalink_ref(self, node: Element, title: str) -> None:
        if node['ids'] and self.config.html_permalinks and self.builder.add_permalinks:
            format = '<a class="headerlink" href="#%s" title="%s">%s</a>'
            self.body.append(format % (node['ids'][0], title,
                                       self.config.html_permalinks_icon))

    def generate_targets_for_listing(self, node: Element) -> None:
        """Generate hyperlink targets for listings.

        Original visit_bullet_list(), visit_definition_list() and visit_enumerated_list()
        generates hyperlink targets inside listing tags (<ul>, <ol> and <dl>) if multiple
        IDs are assigned to listings.  That is invalid DOM structure.
        (This is a bug of docutils <= 0.12)

        This exports hyperlink targets before listings to make valid DOM structure.
        """
        for id in node['ids'][1:]:
            self.body.append('<span id="%s"></span>' % id)
            node['ids'].remove(id)

    # overwritten
    def visit_bullet_list(self, node: Element) -> None:
        if len(node) == 1 and isinstance(node[0], addnodes.toctree):
            # avoid emitting empty <ul></ul>
            raise nodes.SkipNode
        self.generate_targets_for_listing(node)
        super().visit_bullet_list(node)

    # overwritten
    def visit_enumerated_list(self, node: Element) -> None:
        self.generate_targets_for_listing(node)
        super().visit_enumerated_list(node)

    # overwritten
    def visit_definition(self, node: Element) -> None:
        # don't insert </dt> here.
        self.body.append(self.starttag(node, 'dd', ''))

    # overwritten
    def depart_definition(self, node: Element) -> None:
        self.body.append('</dd>\n')

    # overwritten
    def visit_classifier(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))

    # overwritten
    def depart_classifier(self, node: Element) -> None:
        self.body.append('</span>')

        next_node: Node = node.next_node(descend=False, siblings=True)
        if not isinstance(next_node, nodes.classifier):
            # close `<dt>` tag at the tail of classifiers
            self.body.append('</dt>')

    # overwritten
    def visit_term(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'dt', ''))

    # overwritten
    def depart_term(self, node: Element) -> None:
        next_node: Node = node.next_node(descend=False, siblings=True)
        if isinstance(next_node, nodes.classifier):
            # Leave the end tag to `self.depart_classifier()`, in case
            # there's a classifier.
            pass
        else:
            if isinstance(node.parent.parent.parent, addnodes.glossary):
                # add permalink if glossary terms
                self.add_permalink_ref(node, _('Permalink to this term'))

            self.body.append('</dt>')

    # overwritten
    def visit_title(self, node: Element) -> None:
        if isinstance(node.parent, addnodes.compact_paragraph) and node.parent.get('toctree'):
            self.body.append(self.starttag(node, 'p', '', CLASS='caption', ROLE='heading'))
            self.body.append('<span class="caption-text">')
            self.context.append('</span></p>\n')
        else:
            super().visit_title(node)
        self.add_secnumber(node)
        self.add_fignumber(node.parent)
        if isinstance(node.parent, nodes.table):
            self.body.append('<span class="caption-text">')

    def depart_title(self, node: Element) -> None:
        close_tag = self.context[-1]
        if (self.config.html_permalinks and self.builder.add_permalinks and
           node.parent.hasattr('ids') and node.parent['ids']):
            # add permalink anchor
            if close_tag.startswith('</h'):
                self.add_permalink_ref(node.parent, _('Permalink to this heading'))
            elif close_tag.startswith('</a></h'):
                self.body.append('</a><a class="headerlink" href="#%s" ' %
                                 node.parent['ids'][0] +
                                 'title="%s">%s' % (
                                     _('Permalink to this heading'),
                                     self.config.html_permalinks_icon))
            elif isinstance(node.parent, nodes.table):
                self.body.append('</span>')
                self.add_permalink_ref(node.parent, _('Permalink to this table'))
        elif isinstance(node.parent, nodes.table):
            self.body.append('</span>')

        super().depart_title(node)

    # overwritten
    def visit_literal_block(self, node: Element) -> None:
        if node.rawsource != node.astext():
            # most probably a parsed-literal block -- don't highlight
            return super().visit_literal_block(node)

        lang = node.get('language', 'default')
        linenos = node.get('linenos', False)
        highlight_args = node.get('highlight_args', {})
        highlight_args['force'] = node.get('force', False)
        opts = self.config.highlight_options.get(lang, {})

        if linenos and self.config.html_codeblock_linenos_style:
            linenos = self.config.html_codeblock_linenos_style

        highlighted = self.highlighter.highlight_block(
            node.rawsource, lang, opts=opts, linenos=linenos,
            location=node, **highlight_args
        )
        starttag = self.starttag(node, 'div', suffix='',
                                 CLASS='highlight-%s notranslate' % lang)
        self.body.append(starttag + highlighted + '</div>\n')
        raise nodes.SkipNode

    def visit_caption(self, node: Element) -> None:
        if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
            self.body.append('<div class="code-block-caption">')
        else:
            super().visit_caption(node)
        self.add_fignumber(node.parent)
        self.body.append(self.starttag(node, 'span', '', CLASS='caption-text'))

    def depart_caption(self, node: Element) -> None:
        self.body.append('</span>')

        # append permalink if available
        if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
            self.add_permalink_ref(node.parent, _('Permalink to this code'))
        elif isinstance(node.parent, nodes.figure):
            self.add_permalink_ref(node.parent, _('Permalink to this image'))
        elif node.parent.get('toctree'):
            self.add_permalink_ref(node.parent.parent, _('Permalink to this toctree'))

        if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
            self.body.append('</div>\n')
        else:
            super().depart_caption(node)

    def visit_doctest_block(self, node: Element) -> None:
        self.visit_literal_block(node)

    # overwritten to add the <div> (for XHTML compliance)
    def visit_block_quote(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'blockquote') + '<div>')

    def depart_block_quote(self, node: Element) -> None:
        self.body.append('</div></blockquote>\n')

    # overwritten
    def visit_literal(self, node: Element) -> None:
        if 'kbd' in node['classes']:
            self.body.append(self.starttag(node, 'kbd', '',
                                           CLASS='docutils literal notranslate'))
            return
        lang = node.get("language", None)
        if 'code' not in node['classes'] or not lang:
            self.body.append(self.starttag(node, 'code', '',
                                           CLASS='docutils literal notranslate'))
            self.protect_literal_text += 1
            return

        opts = self.config.highlight_options.get(lang, {})
        highlighted = self.highlighter.highlight_block(
            node.astext(), lang, opts=opts, location=node, nowrap=True)
        starttag = self.starttag(
            node,
            "code",
            suffix="",
            CLASS="docutils literal highlight highlight-%s" % lang,
        )
        self.body.append(starttag + highlighted.strip() + "</code>")
        raise nodes.SkipNode

    def depart_literal(self, node: Element) -> None:
        if 'kbd' in node['classes']:
            self.body.append('</kbd>')
        else:
            self.protect_literal_text -= 1
            self.body.append('</code>')

    def visit_productionlist(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'pre'))
        names = []
        productionlist = cast(Iterable[addnodes.production], node)
        for production in productionlist:
            names.append(production['tokenname'])
        maxlen = max(len(name) for name in names)
        lastname = None
        for production in productionlist:
            if production['tokenname']:
                lastname = production['tokenname'].ljust(maxlen)
                self.body.append(self.starttag(production, 'strong', ''))
                self.body.append(lastname + '</strong> ::= ')
            elif lastname is not None:
                self.body.append('%s     ' % (' ' * len(lastname)))
            production.walkabout(self)
            self.body.append('\n')
        self.body.append('</pre>\n')
        raise nodes.SkipNode

    def depart_productionlist(self, node: Element) -> None:
        pass

    def visit_production(self, node: Element) -> None:
        pass

    def depart_production(self, node: Element) -> None:
        pass

    def visit_centered(self, node: Element) -> None:
        self.body.append(self.starttag(node, 'p', CLASS="centered") +
                         '<strong>')

    def depart_centered(self, node: Element) -> None:
        self.body.append('</strong></p>')

    # overwritten
    def should_be_compact_paragraph(self, node: Node) -> bool:
        """Determine if the <p> tags around paragraph can be omitted."""
        if isinstance(node.parent, addnodes.desc_content):
            # Never compact desc_content items.
            return False
        if isinstance(node.parent, addnodes.versionmodified):
            # Never compact versionmodified nodes.
            return False
        return super().should_be_compact_paragraph(node)

    def visit_compact_paragraph(self, node: Element) -> None:
        pass

    def depart_compact_paragraph(self, node: Element) -> None:
        pass

    def visit_download_reference(self, node: Element) -> None:
        atts = {'class': 'reference download',
                'download': ''}

        if not self.builder.download_support:
            self.context.append('')
        elif 'refuri' in node:
            atts['class'] += ' external'
            atts['href'] = node['refuri']
            self.body.append(self.starttag(node, 'a', '', **atts))
            self.context.append('</a>')
        elif 'filename' in node:
            atts['class'] += ' internal'
            atts['href'] = posixpath.join(self.builder.dlpath,
                                          urllib.parse.quote(node['filename']))
            self.body.append(self.starttag(node, 'a', '', **atts))
            self.context.append('</a>')
        else:
            self.context.append('')

    def depart_download_reference(self, node: Element) -> None:
        self.body.append(self.context.pop())

    # overwritten
    def visit_figure(self, node: Element) -> None:
        # set align=default if align not specified to give a default style
        node.setdefault('align', 'default')

        return super().visit_figure(node)

    # overwritten
    def visit_image(self, node: Element) -> None:
        olduri = node['uri']
        # rewrite the URI if the environment knows about it
        if olduri in self.builder.images:
            node['uri'] = posixpath.join(self.builder.imgpath,
                                         urllib.parse.quote(self.builder.images[olduri]))

        if 'scale' in node:
            # Try to figure out image height and width.  Docutils does that too,
            # but it tries the final file name, which does not necessarily exist
            # yet at the time the HTML file is written.
            if not ('width' in node and 'height' in node):
                size = get_image_size(os.path.join(self.builder.srcdir, olduri))
                if size is None:
                    logger.warning(__('Could not obtain image size. :scale: option is ignored.'),  # NOQA
                                   location=node)
                else:
                    if 'width' not in node:
                        node['width'] = str(size[0])
                    if 'height' not in node:
                        node['height'] = str(size[1])

        uri = node['uri']
        if uri.lower().endswith(('svg', 'svgz')):
            atts = {'src': uri}
            if 'width' in node:
                atts['width'] = node['width']
            if 'height' in node:
                atts['height'] = node['height']
            if 'scale' in node:
                if 'width' in atts:
                    atts['width'] = multiply_length(atts['width'], node['scale'])
                if 'height' in atts:
                    atts['height'] = multiply_length(atts['height'], node['scale'])
            atts['alt'] = node.get('alt', uri)
            if 'align' in node:
                atts['class'] = 'align-%s' % node['align']
            self.body.append(self.emptytag(node, 'img', '', **atts))
            return

        super().visit_image(node)

    # overwritten
    def depart_image(self, node: Element) -> None:
        if node['uri'].lower().endswith(('svg', 'svgz')):
            pass
        else:
            super().depart_image(node)

    def visit_toctree(self, node: Element) -> None:
        # this only happens when formatting a toc from env.tocs -- in this
        # case we don't want to include the subtree
        raise nodes.SkipNode

    def visit_index(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_tabular_col_spec(self, node: Element) -> None:
        raise nodes.SkipNode

    def visit_glossary(self, node: Element) -> None:
        pass

    def depart_glossary(self, node: Element) -> None:
        pass

    def visit_acks(self, node: Element) -> None:
        pass

    def depart_acks(self, node: Element) -> None:
        pass

    def visit_hlist(self, node: Element) -> None:
        self.body.append('<table class="hlist"><tr>')

    def depart_hlist(self, node: Element) -> None:
        self.body.append('</tr></table>\n')

    def visit_hlistcol(self, node: Element) -> None:
        self.body.append('<td>')

    def depart_hlistcol(self, node: Element) -> None:
        self.body.append('</td>')

    def visit_option_group(self, node: Element) -> None:
        super().visit_option_group(node)
        self.context[-2] = self.context[-2].replace('&nbsp;', '&#160;')

    # overwritten
    def visit_Text(self, node: Text) -> None:
        text = node.astext()
        encoded = self.encode(text)
        if self.protect_literal_text:
            # moved here from base class's visit_literal to support
            # more formatting in literal nodes
            for token in self.words_and_spaces.findall(encoded):
                if token.strip():
                    # protect literal text from line wrapping
                    self.body.append('<span class="pre">%s</span>' % token)
                elif token in ' \n':
                    # allow breaks at whitespace
                    self.body.append(token)
                else:
                    # protect runs of multiple spaces; the last one can wrap
                    self.body.append('&#160;' * (len(token) - 1) + ' ')
        else:
            if self.in_mailto and self.settings.cloak_email_addresses:
                encoded = self.cloak_email(encoded)
            self.body.append(encoded)

    def visit_note(self, node: Element) -> None:
        self.visit_admonition(node, 'note')

    def depart_note(self, node: Element) -> None:
        self.depart_admonition(node)

    def visit_warning(self, node: Element) -> None:
        self.visit_admonition(node, 'warning')

    def depart_warning(self, node: Element) -> None:
        self.depart_admonition(node)

    def visit_attention(self, node: Element) -> None:
        self.visit_admonition(node, 'attention')

    def depart_attention(self, node: Element) -> None:
        self.depart_admonition(node)

    def visit_caution(self, node: Element) -> None:
        self.visit_admonition(node, 'caution')

    def depart_caution(self, node: Element) -> None:
        self.depart_admonition(node)

    def visit_danger(self, node: Element) -> None:
        self.visit_admonition(node, 'danger')

    def depart_danger(self, node: Element) -> None:
        self.depart_admonition(node)

    def visit_error(self, node: Element) -> None:
        self.visit_admonition(node, 'error')

    def depart_error(self, node: Element) -> None:
        self.depart_admonition(node)

    def visit_hint(self, node: Element) -> None:
        self.visit_admonition(node, 'hint')

    def depart_hint(self, node: Element) -> None:
        self.depart_admonition(node)

    def visit_important(self, node: Element) -> None:
        self.visit_admonition(node, 'important')

    def depart_important(self, node: Element) -> None:
        self.depart_admonition(node)

    def visit_tip(self, node: Element) -> None:
        self.visit_admonition(node, 'tip')

    def depart_tip(self, node: Element) -> None:
        self.depart_admonition(node)

    def visit_literal_emphasis(self, node: Element) -> None:
        return self.visit_emphasis(node)

    def depart_literal_emphasis(self, node: Element) -> None:
        return self.depart_emphasis(node)

    def visit_literal_strong(self, node: Element) -> None:
        return self.visit_strong(node)

    def depart_literal_strong(self, node: Element) -> None:
        return self.depart_strong(node)

    def visit_abbreviation(self, node: Element) -> None:
        attrs = {}
        if node.hasattr('explanation'):
            attrs['title'] = node['explanation']
        self.body.append(self.starttag(node, 'abbr', '', **attrs))

    def depart_abbreviation(self, node: Element) -> None:
        self.body.append('</abbr>')

    def visit_manpage(self, node: Element) -> None:
        self.visit_literal_emphasis(node)
        if self.manpages_url:
            node['refuri'] = self.manpages_url.format(**node.attributes)
            self.visit_reference(node)

    def depart_manpage(self, node: Element) -> None:
        if self.manpages_url:
            self.depart_reference(node)
        self.depart_literal_emphasis(node)

    # overwritten to add even/odd classes

    def visit_table(self, node: Element) -> None:
        self._table_row_indices.append(0)

        # set align=default if align not specified to give a default style
        node.setdefault('align', 'default')

        return super().visit_table(node)

    def depart_table(self, node: Element) -> None:
        self._table_row_indices.pop()
        super().depart_table(node)

    def visit_row(self, node: Element) -> None:
        self._table_row_indices[-1] += 1
        if self._table_row_indices[-1] % 2 == 0:
            node['classes'].append('row-even')
        else:
            node['classes'].append('row-odd')
        self.body.append(self.starttag(node, 'tr', ''))
        node.column = 0  # type: ignore

    def visit_entry(self, node: Element) -> None:
        super().visit_entry(node)
        if self.body[-1] == '&nbsp;':
            self.body[-1] = '&#160;'

    def visit_field_list(self, node: Element) -> None:
        self._fieldlist_row_indices.append(0)
        return super().visit_field_list(node)

    def depart_field_list(self, node: Element) -> None:
        self._fieldlist_row_indices.pop()
        return super().depart_field_list(node)

    def visit_field(self, node: Element) -> None:
        self._fieldlist_row_indices[-1] += 1
        if self._fieldlist_row_indices[-1] % 2 == 0:
            node['classes'].append('field-even')
        else:
            node['classes'].append('field-odd')
        self.body.append(self.starttag(node, 'tr', '', CLASS='field'))

    def visit_field_name(self, node: Element) -> None:
        context_count = len(self.context)
        super().visit_field_name(node)
        if context_count != len(self.context):
            self.context[-1] = self.context[-1].replace('&nbsp;', '&#160;')

    def visit_math(self, node: Element, math_env: str = '') -> None:
        name = self.builder.math_renderer_name
        visit, _ = self.builder.app.registry.html_inline_math_renderers[name]
        visit(self, node)

    def depart_math(self, node: Element, math_env: str = '') -> None:
        name = self.builder.math_renderer_name
        _, depart = self.builder.app.registry.html_inline_math_renderers[name]
        if depart:
            depart(self, node)

    def visit_math_block(self, node: Element, math_env: str = '') -> None:
        name = self.builder.math_renderer_name
        visit, _ = self.builder.app.registry.html_block_math_renderers[name]
        visit(self, node)

    def depart_math_block(self, node: Element, math_env: str = '') -> None:
        name = self.builder.math_renderer_name
        _, depart = self.builder.app.registry.html_block_math_renderers[name]
        if depart:
            depart(self, node)

    @property
    def _fieldlist_row_index(self):
        warnings.warn('_fieldlist_row_index is deprecated',
                      RemovedInSphinx60Warning, stacklevel=2)
        return self._fieldlist_row_indices[-1]

    @property
    def _table_row_index(self):
        warnings.warn('_table_row_index is deprecated',
                      RemovedInSphinx60Warning, stacklevel=2)
        return self._table_row_indices[-1]
