1"""
2    sphinx.writers.html
3    ~~~~~~~~~~~~~~~~~~~
4
5    docutils writers handling Sphinx' custom nodes.
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11import copy
12import os
13import posixpath
14import re
15import warnings
16from typing import Any, Iterable, Tuple, cast
17
18from docutils import nodes
19from docutils.nodes import Element, Node, Text
20from docutils.writers.html4css1 import HTMLTranslator as BaseTranslator
21from docutils.writers.html4css1 import Writer
22
23from sphinx import addnodes
24from sphinx.builders import Builder
25from sphinx.deprecation import RemovedInSphinx40Warning, RemovedInSphinx50Warning
26from sphinx.locale import _, __, admonitionlabels
27from sphinx.util import logging
28from sphinx.util.docutils import SphinxTranslator
29from sphinx.util.images import get_image_size
30
31if False:
32    # For type annotation
33    from sphinx.builders.html import StandaloneHTMLBuilder
34
35
36logger = logging.getLogger(__name__)
37
38# A good overview of the purpose behind these classes can be found here:
39# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html
40
41
42def multiply_length(length: str, scale: int) -> str:
43    """Multiply *length* (width or height) by *scale*."""
44    matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length)
45    if not matched:
46        return length
47    elif scale == 100:
48        return length
49    else:
50        amount, unit = matched.groups()
51        result = float(amount) * scale / 100
52        return "%s%s" % (int(result), unit)
53
54
55class HTMLWriter(Writer):
56
57    # override embed-stylesheet default value to 0.
58    settings_spec = copy.deepcopy(Writer.settings_spec)
59    for _setting in settings_spec[2]:
60        if '--embed-stylesheet' in _setting[1]:
61            _setting[2]['default'] = 0
62
63    def __init__(self, builder: "StandaloneHTMLBuilder") -> None:
64        super().__init__()
65        self.builder = builder
66
67    def translate(self) -> None:
68        # sadly, this is mostly copied from parent class
69        visitor = self.builder.create_translator(self.document, self.builder)
70        self.visitor = cast(HTMLTranslator, visitor)
71        self.document.walkabout(visitor)
72        self.output = self.visitor.astext()
73        for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix',
74                     'body_pre_docinfo', 'docinfo', 'body', 'fragment',
75                     'body_suffix', 'meta', 'title', 'subtitle', 'header',
76                     'footer', 'html_prolog', 'html_head', 'html_title',
77                     'html_subtitle', 'html_body', ):
78            setattr(self, attr, getattr(visitor, attr, None))
79        self.clean_meta = ''.join(self.visitor.meta[2:])
80
81
82class HTMLTranslator(SphinxTranslator, BaseTranslator):
83    """
84    Our custom HTML translator.
85    """
86
87    builder = None  # type: StandaloneHTMLBuilder
88
89    def __init__(self, *args: Any) -> None:
90        if isinstance(args[0], nodes.document) and isinstance(args[1], Builder):
91            document, builder = args
92        else:
93            warnings.warn('The order of arguments for HTMLTranslator has been changed. '
94                          'Please give "document" as 1st and "builder" as 2nd.',
95                          RemovedInSphinx40Warning, stacklevel=2)
96            builder, document = args
97        super().__init__(document, builder)
98
99        self.highlighter = self.builder.highlighter
100        self.docnames = [self.builder.current_docname]  # for singlehtml builder
101        self.manpages_url = self.config.manpages_url
102        self.protect_literal_text = 0
103        self.secnumber_suffix = self.config.html_secnumber_suffix
104        self.param_separator = ''
105        self.optional_param_level = 0
106        self._table_row_index = 0
107        self._fieldlist_row_index = 0
108        self.required_params_left = 0
109
110    def visit_start_of_file(self, node: Element) -> None:
111        # only occurs in the single-file builder
112        self.docnames.append(node['docname'])
113        self.body.append('<span id="document-%s"></span>' % node['docname'])
114
115    def depart_start_of_file(self, node: Element) -> None:
116        self.docnames.pop()
117
118    def visit_desc(self, node: Element) -> None:
119        self.body.append(self.starttag(node, 'dl', CLASS=node['objtype']))
120
121    def depart_desc(self, node: Element) -> None:
122        self.body.append('</dl>\n\n')
123
124    def visit_desc_signature(self, node: Element) -> None:
125        # the id is set automatically
126        self.body.append(self.starttag(node, 'dt'))
127        self.protect_literal_text += 1
128
129    def depart_desc_signature(self, node: Element) -> None:
130        self.protect_literal_text -= 1
131        if not node.get('is_multiline'):
132            self.add_permalink_ref(node, _('Permalink to this definition'))
133        self.body.append('</dt>\n')
134
135    def visit_desc_signature_line(self, node: Element) -> None:
136        pass
137
138    def depart_desc_signature_line(self, node: Element) -> None:
139        if node.get('add_permalink'):
140            # the permalink info is on the parent desc_signature node
141            self.add_permalink_ref(node.parent, _('Permalink to this definition'))
142        self.body.append('<br />')
143
144    def visit_desc_addname(self, node: Element) -> None:
145        self.body.append(self.starttag(node, 'code', '', CLASS='descclassname'))
146
147    def depart_desc_addname(self, node: Element) -> None:
148        self.body.append('</code>')
149
150    def visit_desc_type(self, node: Element) -> None:
151        pass
152
153    def depart_desc_type(self, node: Element) -> None:
154        pass
155
156    def visit_desc_returns(self, node: Element) -> None:
157        self.body.append(' &#x2192; ')
158
159    def depart_desc_returns(self, node: Element) -> None:
160        pass
161
162    def visit_desc_name(self, node: Element) -> None:
163        self.body.append(self.starttag(node, 'code', '', CLASS='descname'))
164
165    def depart_desc_name(self, node: Element) -> None:
166        self.body.append('</code>')
167
168    def visit_desc_parameterlist(self, node: Element) -> None:
169        self.body.append('<span class="sig-paren">(</span>')
170        self.first_param = 1
171        self.optional_param_level = 0
172        # How many required parameters are left.
173        self.required_params_left = sum([isinstance(c, addnodes.desc_parameter)
174                                         for c in node.children])
175        self.param_separator = node.child_text_separator
176
177    def depart_desc_parameterlist(self, node: Element) -> None:
178        self.body.append('<span class="sig-paren">)</span>')
179
180    # If required parameters are still to come, then put the comma after
181    # the parameter.  Otherwise, put the comma before.  This ensures that
182    # signatures like the following render correctly (see issue #1001):
183    #
184    #     foo([a, ]b, c[, d])
185    #
186    def visit_desc_parameter(self, node: Element) -> None:
187        if self.first_param:
188            self.first_param = 0
189        elif not self.required_params_left:
190            self.body.append(self.param_separator)
191        if self.optional_param_level == 0:
192            self.required_params_left -= 1
193        if not node.hasattr('noemph'):
194            self.body.append('<em>')
195
196    def depart_desc_parameter(self, node: Element) -> None:
197        if not node.hasattr('noemph'):
198            self.body.append('</em>')
199        if self.required_params_left:
200            self.body.append(self.param_separator)
201
202    def visit_desc_optional(self, node: Element) -> None:
203        self.optional_param_level += 1
204        self.body.append('<span class="optional">[</span>')
205
206    def depart_desc_optional(self, node: Element) -> None:
207        self.optional_param_level -= 1
208        self.body.append('<span class="optional">]</span>')
209
210    def visit_desc_annotation(self, node: Element) -> None:
211        self.body.append(self.starttag(node, 'em', '', CLASS='property'))
212
213    def depart_desc_annotation(self, node: Element) -> None:
214        self.body.append('</em>')
215
216    def visit_desc_content(self, node: Element) -> None:
217        self.body.append(self.starttag(node, 'dd', ''))
218
219    def depart_desc_content(self, node: Element) -> None:
220        self.body.append('</dd>')
221
222    def visit_versionmodified(self, node: Element) -> None:
223        self.body.append(self.starttag(node, 'div', CLASS=node['type']))
224
225    def depart_versionmodified(self, node: Element) -> None:
226        self.body.append('</div>\n')
227
228    # overwritten
229    def visit_reference(self, node: Element) -> None:
230        atts = {'class': 'reference'}
231        if node.get('internal') or 'refuri' not in node:
232            atts['class'] += ' internal'
233        else:
234            atts['class'] += ' external'
235        if 'refuri' in node:
236            atts['href'] = node['refuri'] or '#'
237            if self.settings.cloak_email_addresses and atts['href'].startswith('mailto:'):
238                atts['href'] = self.cloak_mailto(atts['href'])
239                self.in_mailto = True
240        else:
241            assert 'refid' in node, \
242                   'References must have "refuri" or "refid" attribute.'
243            atts['href'] = '#' + node['refid']
244        if not isinstance(node.parent, nodes.TextElement):
245            assert len(node) == 1 and isinstance(node[0], nodes.image)
246            atts['class'] += ' image-reference'
247        if 'reftitle' in node:
248            atts['title'] = node['reftitle']
249        if 'target' in node:
250            atts['target'] = node['target']
251        self.body.append(self.starttag(node, 'a', '', **atts))
252
253        if node.get('secnumber'):
254            self.body.append(('%s' + self.secnumber_suffix) %
255                             '.'.join(map(str, node['secnumber'])))
256
257    def visit_number_reference(self, node: Element) -> None:
258        self.visit_reference(node)
259
260    def depart_number_reference(self, node: Element) -> None:
261        self.depart_reference(node)
262
263    # overwritten -- we don't want source comments to show up in the HTML
264    def visit_comment(self, node: Element) -> None:  # type: ignore
265        raise nodes.SkipNode
266
267    # overwritten
268    def visit_admonition(self, node: Element, name: str = '') -> None:
269        self.body.append(self.starttag(
270            node, 'div', CLASS=('admonition ' + name)))
271        if name:
272            node.insert(0, nodes.title(name, admonitionlabels[name]))
273        self.set_first_last(node)
274
275    def visit_seealso(self, node: Element) -> None:
276        self.visit_admonition(node, 'seealso')
277
278    def depart_seealso(self, node: Element) -> None:
279        self.depart_admonition(node)
280
281    def get_secnumber(self, node: Element) -> Tuple[int, ...]:
282        if node.get('secnumber'):
283            return node['secnumber']
284        elif isinstance(node.parent, nodes.section):
285            if self.builder.name == 'singlehtml':
286                docname = self.docnames[-1]
287                anchorname = "%s/#%s" % (docname, node.parent['ids'][0])
288                if anchorname not in self.builder.secnumbers:
289                    anchorname = "%s/" % docname  # try first heading which has no anchor
290            else:
291                anchorname = '#' + node.parent['ids'][0]
292                if anchorname not in self.builder.secnumbers:
293                    anchorname = ''  # try first heading which has no anchor
294
295            if self.builder.secnumbers.get(anchorname):
296                return self.builder.secnumbers[anchorname]
297
298        return None
299
300    def add_secnumber(self, node: Element) -> None:
301        secnumber = self.get_secnumber(node)
302        if secnumber:
303            self.body.append('<span class="section-number">%s</span>' %
304                             ('.'.join(map(str, secnumber)) + self.secnumber_suffix))
305
306    def add_fignumber(self, node: Element) -> None:
307        def append_fignumber(figtype: str, figure_id: str) -> None:
308            if self.builder.name == 'singlehtml':
309                key = "%s/%s" % (self.docnames[-1], figtype)
310            else:
311                key = figtype
312
313            if figure_id in self.builder.fignumbers.get(key, {}):
314                self.body.append('<span class="caption-number">')
315                prefix = self.config.numfig_format.get(figtype)
316                if prefix is None:
317                    msg = __('numfig_format is not defined for %s') % figtype
318                    logger.warning(msg)
319                else:
320                    numbers = self.builder.fignumbers[key][figure_id]
321                    self.body.append(prefix % '.'.join(map(str, numbers)) + ' ')
322                    self.body.append('</span>')
323
324        figtype = self.builder.env.domains['std'].get_enumerable_node_type(node)
325        if figtype:
326            if len(node['ids']) == 0:
327                msg = __('Any IDs not assigned for %s node') % node.tagname
328                logger.warning(msg, location=node)
329            else:
330                append_fignumber(figtype, node['ids'][0])
331
332    def add_permalink_ref(self, node: Element, title: str) -> None:
333        if node['ids'] and self.config.html_permalinks and self.builder.add_permalinks:
334            format = '<a class="headerlink" href="#%s" title="%s">%s</a>'
335            self.body.append(format % (node['ids'][0], title,
336                                       self.config.html_permalinks_icon))
337
338    def generate_targets_for_listing(self, node: Element) -> None:
339        """Generate hyperlink targets for listings.
340
341        Original visit_bullet_list(), visit_definition_list() and visit_enumerated_list()
342        generates hyperlink targets inside listing tags (<ul>, <ol> and <dl>) if multiple
343        IDs are assigned to listings.  That is invalid DOM structure.
344        (This is a bug of docutils <= 0.12)
345
346        This exports hyperlink targets before listings to make valid DOM structure.
347        """
348        for id in node['ids'][1:]:
349            self.body.append('<span id="%s"></span>' % id)
350            node['ids'].remove(id)
351
352    # overwritten
353    def visit_bullet_list(self, node: Element) -> None:
354        if len(node) == 1 and isinstance(node[0], addnodes.toctree):
355            # avoid emitting empty <ul></ul>
356            raise nodes.SkipNode
357        self.generate_targets_for_listing(node)
358        super().visit_bullet_list(node)
359
360    # overwritten
361    def visit_enumerated_list(self, node: Element) -> None:
362        self.generate_targets_for_listing(node)
363        super().visit_enumerated_list(node)
364
365    # overwritten
366    def visit_definition(self, node: Element) -> None:
367        # don't insert </dt> here.
368        self.body.append(self.starttag(node, 'dd', ''))
369
370    # overwritten
371    def depart_definition(self, node: Element) -> None:
372        self.body.append('</dd>\n')
373
374    # overwritten
375    def visit_classifier(self, node: Element) -> None:
376        self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
377
378    # overwritten
379    def depart_classifier(self, node: Element) -> None:
380        self.body.append('</span>')
381
382        next_node = node.next_node(descend=False, siblings=True)  # type: Node
383        if not isinstance(next_node, nodes.classifier):
384            # close `<dt>` tag at the tail of classifiers
385            self.body.append('</dt>')
386
387    # overwritten
388    def visit_term(self, node: Element) -> None:
389        self.body.append(self.starttag(node, 'dt', ''))
390
391    # overwritten
392    def depart_term(self, node: Element) -> None:
393        next_node = node.next_node(descend=False, siblings=True)  # type: Node
394        if isinstance(next_node, nodes.classifier):
395            # Leave the end tag to `self.depart_classifier()`, in case
396            # there's a classifier.
397            pass
398        else:
399            if isinstance(node.parent.parent.parent, addnodes.glossary):
400                # add permalink if glossary terms
401                self.add_permalink_ref(node, _('Permalink to this term'))
402
403            self.body.append('</dt>')
404
405    # overwritten
406    def visit_title(self, node: Element) -> None:
407        super().visit_title(node)
408        self.add_secnumber(node)
409        self.add_fignumber(node.parent)
410        if isinstance(node.parent, nodes.table):
411            self.body.append('<span class="caption-text">')
412
413    def depart_title(self, node: Element) -> None:
414        close_tag = self.context[-1]
415        if (self.config.html_permalinks and self.builder.add_permalinks and
416           node.parent.hasattr('ids') and node.parent['ids']):
417            # add permalink anchor
418            if close_tag.startswith('</h'):
419                self.add_permalink_ref(node.parent, _('Permalink to this headline'))
420            elif close_tag.startswith('</a></h'):
421                self.body.append('</a><a class="headerlink" href="#%s" ' %
422                                 node.parent['ids'][0] +
423                                 'title="%s">%s' % (
424                                     _('Permalink to this headline'),
425                                     self.config.html_permalinks_icon))
426            elif isinstance(node.parent, nodes.table):
427                self.body.append('</span>')
428                self.add_permalink_ref(node.parent, _('Permalink to this table'))
429        elif isinstance(node.parent, nodes.table):
430            self.body.append('</span>')
431
432        super().depart_title(node)
433
434    # overwritten
435    def visit_literal_block(self, node: Element) -> None:
436        if node.rawsource != node.astext():
437            # most probably a parsed-literal block -- don't highlight
438            return super().visit_literal_block(node)
439
440        lang = node.get('language', 'default')
441        linenos = node.get('linenos', False)
442        highlight_args = node.get('highlight_args', {})
443        highlight_args['force'] = node.get('force', False)
444        opts = self.config.highlight_options.get(lang, {})
445
446        if linenos and self.config.html_codeblock_linenos_style:
447            linenos = self.config.html_codeblock_linenos_style
448
449        highlighted = self.highlighter.highlight_block(
450            node.rawsource, lang, opts=opts, linenos=linenos,
451            location=node, **highlight_args
452        )
453        starttag = self.starttag(node, 'div', suffix='',
454                                 CLASS='highlight-%s notranslate' % lang)
455        self.body.append(starttag + highlighted + '</div>\n')
456        raise nodes.SkipNode
457
458    def visit_caption(self, node: Element) -> None:
459        if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
460            self.body.append('<div class="code-block-caption">')
461        else:
462            super().visit_caption(node)
463        self.add_fignumber(node.parent)
464        self.body.append(self.starttag(node, 'span', '', CLASS='caption-text'))
465
466    def depart_caption(self, node: Element) -> None:
467        self.body.append('</span>')
468
469        # append permalink if available
470        if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
471            self.add_permalink_ref(node.parent, _('Permalink to this code'))
472        elif isinstance(node.parent, nodes.figure):
473            self.add_permalink_ref(node.parent, _('Permalink to this image'))
474        elif node.parent.get('toctree'):
475            self.add_permalink_ref(node.parent.parent, _('Permalink to this toctree'))
476
477        if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
478            self.body.append('</div>\n')
479        else:
480            super().depart_caption(node)
481
482    def visit_doctest_block(self, node: Element) -> None:
483        self.visit_literal_block(node)
484
485    # overwritten to add the <div> (for XHTML compliance)
486    def visit_block_quote(self, node: Element) -> None:
487        self.body.append(self.starttag(node, 'blockquote') + '<div>')
488
489    def depart_block_quote(self, node: Element) -> None:
490        self.body.append('</div></blockquote>\n')
491
492    # overwritten
493    def visit_literal(self, node: Element) -> None:
494        if 'kbd' in node['classes']:
495            self.body.append(self.starttag(node, 'kbd', '',
496                                           CLASS='docutils literal notranslate'))
497        else:
498            self.body.append(self.starttag(node, 'code', '',
499                                           CLASS='docutils literal notranslate'))
500            self.protect_literal_text += 1
501
502    def depart_literal(self, node: Element) -> None:
503        if 'kbd' in node['classes']:
504            self.body.append('</kbd>')
505        else:
506            self.protect_literal_text -= 1
507            self.body.append('</code>')
508
509    def visit_productionlist(self, node: Element) -> None:
510        self.body.append(self.starttag(node, 'pre'))
511        names = []
512        productionlist = cast(Iterable[addnodes.production], node)
513        for production in productionlist:
514            names.append(production['tokenname'])
515        maxlen = max(len(name) for name in names)
516        lastname = None
517        for production in productionlist:
518            if production['tokenname']:
519                lastname = production['tokenname'].ljust(maxlen)
520                self.body.append(self.starttag(production, 'strong', ''))
521                self.body.append(lastname + '</strong> ::= ')
522            elif lastname is not None:
523                self.body.append('%s     ' % (' ' * len(lastname)))
524            production.walkabout(self)
525            self.body.append('\n')
526        self.body.append('</pre>\n')
527        raise nodes.SkipNode
528
529    def depart_productionlist(self, node: Element) -> None:
530        pass
531
532    def visit_production(self, node: Element) -> None:
533        pass
534
535    def depart_production(self, node: Element) -> None:
536        pass
537
538    def visit_centered(self, node: Element) -> None:
539        self.body.append(self.starttag(node, 'p', CLASS="centered") +
540                         '<strong>')
541
542    def depart_centered(self, node: Element) -> None:
543        self.body.append('</strong></p>')
544
545    # overwritten
546    def should_be_compact_paragraph(self, node: Node) -> bool:
547        """Determine if the <p> tags around paragraph can be omitted."""
548        if isinstance(node.parent, addnodes.desc_content):
549            # Never compact desc_content items.
550            return False
551        if isinstance(node.parent, addnodes.versionmodified):
552            # Never compact versionmodified nodes.
553            return False
554        return super().should_be_compact_paragraph(node)
555
556    def visit_compact_paragraph(self, node: Element) -> None:
557        pass
558
559    def depart_compact_paragraph(self, node: Element) -> None:
560        pass
561
562    def visit_download_reference(self, node: Element) -> None:
563        atts = {'class': 'reference download',
564                'download': ''}
565
566        if not self.builder.download_support:
567            self.context.append('')
568        elif 'refuri' in node:
569            atts['class'] += ' external'
570            atts['href'] = node['refuri']
571            self.body.append(self.starttag(node, 'a', '', **atts))
572            self.context.append('</a>')
573        elif 'filename' in node:
574            atts['class'] += ' internal'
575            atts['href'] = posixpath.join(self.builder.dlpath, node['filename'])
576            self.body.append(self.starttag(node, 'a', '', **atts))
577            self.context.append('</a>')
578        else:
579            self.context.append('')
580
581    def depart_download_reference(self, node: Element) -> None:
582        self.body.append(self.context.pop())
583
584    # overwritten
585    def visit_image(self, node: Element) -> None:
586        olduri = node['uri']
587        # rewrite the URI if the environment knows about it
588        if olduri in self.builder.images:
589            node['uri'] = posixpath.join(self.builder.imgpath,
590                                         self.builder.images[olduri])
591
592        if 'scale' in node:
593            # Try to figure out image height and width.  Docutils does that too,
594            # but it tries the final file name, which does not necessarily exist
595            # yet at the time the HTML file is written.
596            if not ('width' in node and 'height' in node):
597                size = get_image_size(os.path.join(self.builder.srcdir, olduri))
598                if size is None:
599                    logger.warning(__('Could not obtain image size. :scale: option is ignored.'),  # NOQA
600                                   location=node)
601                else:
602                    if 'width' not in node:
603                        node['width'] = str(size[0])
604                    if 'height' not in node:
605                        node['height'] = str(size[1])
606
607        uri = node['uri']
608        if uri.lower().endswith(('svg', 'svgz')):
609            atts = {'src': uri}
610            if 'width' in node:
611                atts['width'] = node['width']
612            if 'height' in node:
613                atts['height'] = node['height']
614            if 'scale' in node:
615                if 'width' in atts:
616                    atts['width'] = multiply_length(atts['width'], node['scale'])
617                if 'height' in atts:
618                    atts['height'] = multiply_length(atts['height'], node['scale'])
619            atts['alt'] = node.get('alt', uri)
620            if 'align' in node:
621                atts['class'] = 'align-%s' % node['align']
622            self.body.append(self.emptytag(node, 'img', '', **atts))
623            return
624
625        super().visit_image(node)
626
627    # overwritten
628    def depart_image(self, node: Element) -> None:
629        if node['uri'].lower().endswith(('svg', 'svgz')):
630            pass
631        else:
632            super().depart_image(node)
633
634    def visit_toctree(self, node: Element) -> None:
635        # this only happens when formatting a toc from env.tocs -- in this
636        # case we don't want to include the subtree
637        raise nodes.SkipNode
638
639    def visit_index(self, node: Element) -> None:
640        raise nodes.SkipNode
641
642    def visit_tabular_col_spec(self, node: Element) -> None:
643        raise nodes.SkipNode
644
645    def visit_glossary(self, node: Element) -> None:
646        pass
647
648    def depart_glossary(self, node: Element) -> None:
649        pass
650
651    def visit_acks(self, node: Element) -> None:
652        pass
653
654    def depart_acks(self, node: Element) -> None:
655        pass
656
657    def visit_hlist(self, node: Element) -> None:
658        self.body.append('<table class="hlist"><tr>')
659
660    def depart_hlist(self, node: Element) -> None:
661        self.body.append('</tr></table>\n')
662
663    def visit_hlistcol(self, node: Element) -> None:
664        self.body.append('<td>')
665
666    def depart_hlistcol(self, node: Element) -> None:
667        self.body.append('</td>')
668
669    def visit_option_group(self, node: Element) -> None:
670        super().visit_option_group(node)
671        self.context[-2] = self.context[-2].replace('&nbsp;', '&#160;')
672
673    # overwritten
674    def visit_Text(self, node: Text) -> None:
675        text = node.astext()
676        encoded = self.encode(text)
677        if self.protect_literal_text:
678            # moved here from base class's visit_literal to support
679            # more formatting in literal nodes
680            for token in self.words_and_spaces.findall(encoded):
681                if token.strip():
682                    # protect literal text from line wrapping
683                    self.body.append('<span class="pre">%s</span>' % token)
684                elif token in ' \n':
685                    # allow breaks at whitespace
686                    self.body.append(token)
687                else:
688                    # protect runs of multiple spaces; the last one can wrap
689                    self.body.append('&#160;' * (len(token) - 1) + ' ')
690        else:
691            if self.in_mailto and self.settings.cloak_email_addresses:
692                encoded = self.cloak_email(encoded)
693            self.body.append(encoded)
694
695    def visit_note(self, node: Element) -> None:
696        self.visit_admonition(node, 'note')
697
698    def depart_note(self, node: Element) -> None:
699        self.depart_admonition(node)
700
701    def visit_warning(self, node: Element) -> None:
702        self.visit_admonition(node, 'warning')
703
704    def depart_warning(self, node: Element) -> None:
705        self.depart_admonition(node)
706
707    def visit_attention(self, node: Element) -> None:
708        self.visit_admonition(node, 'attention')
709
710    def depart_attention(self, node: Element) -> None:
711        self.depart_admonition(node)
712
713    def visit_caution(self, node: Element) -> None:
714        self.visit_admonition(node, 'caution')
715
716    def depart_caution(self, node: Element) -> None:
717        self.depart_admonition(node)
718
719    def visit_danger(self, node: Element) -> None:
720        self.visit_admonition(node, 'danger')
721
722    def depart_danger(self, node: Element) -> None:
723        self.depart_admonition(node)
724
725    def visit_error(self, node: Element) -> None:
726        self.visit_admonition(node, 'error')
727
728    def depart_error(self, node: Element) -> None:
729        self.depart_admonition(node)
730
731    def visit_hint(self, node: Element) -> None:
732        self.visit_admonition(node, 'hint')
733
734    def depart_hint(self, node: Element) -> None:
735        self.depart_admonition(node)
736
737    def visit_important(self, node: Element) -> None:
738        self.visit_admonition(node, 'important')
739
740    def depart_important(self, node: Element) -> None:
741        self.depart_admonition(node)
742
743    def visit_tip(self, node: Element) -> None:
744        self.visit_admonition(node, 'tip')
745
746    def depart_tip(self, node: Element) -> None:
747        self.depart_admonition(node)
748
749    def visit_literal_emphasis(self, node: Element) -> None:
750        return self.visit_emphasis(node)
751
752    def depart_literal_emphasis(self, node: Element) -> None:
753        return self.depart_emphasis(node)
754
755    def visit_literal_strong(self, node: Element) -> None:
756        return self.visit_strong(node)
757
758    def depart_literal_strong(self, node: Element) -> None:
759        return self.depart_strong(node)
760
761    def visit_abbreviation(self, node: Element) -> None:
762        attrs = {}
763        if node.hasattr('explanation'):
764            attrs['title'] = node['explanation']
765        self.body.append(self.starttag(node, 'abbr', '', **attrs))
766
767    def depart_abbreviation(self, node: Element) -> None:
768        self.body.append('</abbr>')
769
770    def visit_manpage(self, node: Element) -> None:
771        self.visit_literal_emphasis(node)
772        if self.manpages_url:
773            node['refuri'] = self.manpages_url.format(**node.attributes)
774            self.visit_reference(node)
775
776    def depart_manpage(self, node: Element) -> None:
777        if self.manpages_url:
778            self.depart_reference(node)
779        self.depart_literal_emphasis(node)
780
781    # overwritten to add even/odd classes
782
783    def visit_table(self, node: Element) -> None:
784        self._table_row_index = 0
785        return super().visit_table(node)
786
787    def visit_row(self, node: Element) -> None:
788        self._table_row_index += 1
789        if self._table_row_index % 2 == 0:
790            node['classes'].append('row-even')
791        else:
792            node['classes'].append('row-odd')
793        self.body.append(self.starttag(node, 'tr', ''))
794        node.column = 0  # type: ignore
795
796    def visit_entry(self, node: Element) -> None:
797        super().visit_entry(node)
798        if self.body[-1] == '&nbsp;':
799            self.body[-1] = '&#160;'
800
801    def visit_field_list(self, node: Element) -> None:
802        self._fieldlist_row_index = 0
803        return super().visit_field_list(node)
804
805    def visit_field(self, node: Element) -> None:
806        self._fieldlist_row_index += 1
807        if self._fieldlist_row_index % 2 == 0:
808            node['classes'].append('field-even')
809        else:
810            node['classes'].append('field-odd')
811        self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
812
813    def visit_field_name(self, node: Element) -> None:
814        context_count = len(self.context)
815        super().visit_field_name(node)
816        if context_count != len(self.context):
817            self.context[-1] = self.context[-1].replace('&nbsp;', '&#160;')
818
819    def visit_math(self, node: Element, math_env: str = '') -> None:
820        name = self.builder.math_renderer_name
821        visit, _ = self.builder.app.registry.html_inline_math_renderers[name]
822        visit(self, node)
823
824    def depart_math(self, node: Element, math_env: str = '') -> None:
825        name = self.builder.math_renderer_name
826        _, depart = self.builder.app.registry.html_inline_math_renderers[name]
827        if depart:
828            depart(self, node)
829
830    def visit_math_block(self, node: Element, math_env: str = '') -> None:
831        name = self.builder.math_renderer_name
832        visit, _ = self.builder.app.registry.html_block_math_renderers[name]
833        visit(self, node)
834
835    def depart_math_block(self, node: Element, math_env: str = '') -> None:
836        name = self.builder.math_renderer_name
837        _, depart = self.builder.app.registry.html_block_math_renderers[name]
838        if depart:
839            depart(self, node)
840
841    def unknown_visit(self, node: Node) -> None:
842        raise NotImplementedError('Unknown node: ' + node.__class__.__name__)
843
844    @property
845    def permalink_text(self) -> str:
846        warnings.warn('HTMLTranslator.permalink_text is deprecated.',
847                      RemovedInSphinx50Warning, stacklevel=2)
848        return self.config.html_permalinks_icon
849