"""Experimental docutils writers for HTML5 handling Sphinx's custom nodes."""

from __future__ import annotations

import posixpath
import re
import urllib.parse
from typing import TYPE_CHECKING

from docutils import nodes
from docutils.writers.html5_polyglot import HTMLTranslator as BaseTranslator

from sphinx import addnodes
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 collections.abc import Callable

    from docutils.nodes import Element, Node, Text

    from sphinx.builders import Builder
    from sphinx.builders.html import StandaloneHTMLBuilder


logger = logging.getLogger(__name__)

# A good overview of the purpose behind these classes can be found here:
# https://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
    if scale == 100:
        return length
    amount, unit = matched.groups()
    result = float(amount) * scale / 100
    return f'{int(result)}{unit}'


class HTML5Translator(SphinxTranslator, BaseTranslator):
    """Our custom HTML translator."""

    builder: StandaloneHTMLBuilder
    # Override docutils.writers.html5_polyglot:HTMLTranslator
    # otherwise, nodes like ... will be
    # converted to ... by `visit_inline`.
    supported_inline_tags: set[str] = set()

    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.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
        self._has_maths_elements: bool = False

    def visit_start_of_file(self, node: Element) -> None:
        # only occurs in the single-file builder
        self.docnames.append(node['docname'])
        self.body.append('' % 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('\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, _('Link to this definition'))
        self.body.append('\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, _('Link to this definition'))
        self.body.append('
') 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('') 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('') # Nodes for high-level structure in signatures ############################################## def visit_desc_name(self, node: Element) -> None: self.body.append(self.starttag(node, 'span', '')) def depart_desc_name(self, node: Element) -> None: self.body.append('') def visit_desc_addname(self, node: Element) -> None: self.body.append(self.starttag(node, 'span', '')) def depart_desc_addname(self, node: Element) -> None: self.body.append('') 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(' ') self.body.append('') self.body.append(' ') def depart_desc_returns(self, node: Element) -> None: self.body.append('') def _visit_sig_parameter_list( self, node: Element, parameter_group: type[Element], sig_open_paren: str, sig_close_paren: str, ) -> None: """Visit a signature parameters or type parameters list. The *parameter_group* value is the type of child nodes acting as required parameters or as a set of contiguous optional parameters. """ self.body.append(f'{sig_open_paren}') self.is_first_param = True self.optional_param_level = 0 self.params_left_at_level = 0 self.param_group_index = 0 # Counts as what we call a parameter group either a required parameter, or a # set of contiguous optional ones. self.list_is_required_param = [ isinstance(c, parameter_group) for c in node.children ] # How many required parameters are left. self.required_params_left = sum(self.list_is_required_param) self.param_separator = node.child_text_separator self.multi_line_parameter_list = node.get('multi_line_parameter_list', False) self.trailing_comma = node.get('multi_line_trailing_comma', False) if self.multi_line_parameter_list: self.body.append('\n\n') self.body.append(self.starttag(node, 'dl')) self.param_separator = self.param_separator.rstrip() self.context.append(sig_close_paren) def _depart_sig_parameter_list(self, node: Element) -> None: if node.get('multi_line_parameter_list'): self.body.append('\n\n') sig_close_paren = self.context.pop() self.body.append(f'{sig_close_paren}') def visit_desc_parameterlist(self, node: Element) -> None: self._visit_sig_parameter_list(node, addnodes.desc_parameter, '(', ')') def depart_desc_parameterlist(self, node: Element) -> None: self._depart_sig_parameter_list(node) def visit_desc_type_parameter_list(self, node: Element) -> None: self._visit_sig_parameter_list(node, addnodes.desc_type_parameter, '[', ']') def depart_desc_type_parameter_list(self, node: Element) -> None: self._depart_sig_parameter_list(node) # 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: # # foo([a, ]b, c[, d]) # # See: https://github.com/sphinx-doc/sphinx/issues/1001 def visit_desc_parameter(self, node: Element) -> None: on_separate_line = self.multi_line_parameter_list if on_separate_line and not ( self.is_first_param and self.optional_param_level > 0 ): self.body.append(self.starttag(node, 'dd', '')) if self.is_first_param: self.is_first_param = False elif not on_separate_line and not self.required_params_left: self.body.append(self.param_separator) if self.optional_param_level == 0: self.required_params_left -= 1 else: self.params_left_at_level -= 1 if not node.hasattr('noemph'): self.body.append('') def depart_desc_parameter(self, node: Element) -> None: if not node.hasattr('noemph'): self.body.append('') is_required = self.list_is_required_param[self.param_group_index] if self.multi_line_parameter_list: len_lirp = len(self.list_is_required_param) is_last_group = self.param_group_index + 1 == len_lirp next_is_required = ( not is_last_group and self.list_is_required_param[self.param_group_index + 1] ) opt_param_left_at_level = self.params_left_at_level > 0 if ( opt_param_left_at_level or is_required and (is_last_group or next_is_required) ): if not is_last_group or opt_param_left_at_level or self.trailing_comma: self.body.append(self.param_separator) self.body.append('\n') elif self.required_params_left: self.body.append(self.param_separator) if is_required: self.param_group_index += 1 def visit_desc_type_parameter(self, node: Element) -> None: self.visit_desc_parameter(node) def depart_desc_type_parameter(self, node: Element) -> None: self.depart_desc_parameter(node) def visit_desc_optional(self, node: Element) -> None: self.params_left_at_level = sum( isinstance(c, addnodes.desc_parameter) for c in node.children ) self.optional_param_level += 1 self.max_optional_param_level = self.optional_param_level if self.multi_line_parameter_list: # If the first parameter is optional, start a new line and open the bracket. if self.is_first_param: self.body.append(self.starttag(node, 'dd', '')) self.body.append('[') # Else, if there remains at least one required parameter, append the # parameter separator, open a new bracket, and end the line. elif self.required_params_left: self.body.append(self.param_separator) self.body.append('[') self.body.append('\n') # Else, open a new bracket, append the parameter separator, # and end the line. else: self.body.append('[') self.body.append(self.param_separator) self.body.append('\n') else: self.body.append('[') def depart_desc_optional(self, node: Element) -> None: self.optional_param_level -= 1 level = self.optional_param_level if self.multi_line_parameter_list: max_level = self.max_optional_param_level len_lirp = len(self.list_is_required_param) is_last_group = self.param_group_index + 1 == len_lirp # If it's the first time we go down one level, add the separator before the # bracket, except if this is the last parameter and the parameter list # should not feature a trailing comma. if level == max_level - 1 and ( not is_last_group or level > 0 or self.trailing_comma ): self.body.append(self.param_separator) self.body.append(']') # End the line if we have just closed the last bracket of this # optional parameter group. if level == 0: self.body.append('\n') else: self.body.append(']') if level == 0: self.param_group_index += 1 def visit_desc_annotation(self, node: Element) -> None: self.body.append(self.starttag(node, 'span', '', CLASS='property')) def depart_desc_annotation(self, node: Element) -> None: self.body.append('') ############################################## 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('\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:') ): # fmt: skip 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) # NoQA: PT018 atts['class'] += ' image-reference' if 'reftitle' in node: atts['title'] = node['reftitle'] if 'target' in node: atts['target'] = node['target'] if 'rel' in node: atts['rel'] = node['rel'] # TODO: TYPING: Upstream docutils should type HTMLTranslator.starttag() # to accept **attributes; current stub treats the 4th parameter # as empty: bool. self.body.append(self.starttag(node, 'a', '', **atts)) # type: ignore[arg-type] 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: nodes.reference) -> None: self.depart_reference(node) # overwritten -- we don't want source comments to show up in the HTML def visit_comment( self, node: nodes.comment, sub: Callable[[str, str], str] | None = None ) -> None: raise nodes.SkipNode # overwritten def visit_admonition(self, node: Element, name: str = '') -> None: attributes = {} tag_name = 'div' if collapsible := node.get('collapsible'): tag_name = 'details' if collapsible == 'open': attributes['open'] = 'open' # TODO: TYPING: Upstream docutils should type HTMLTranslator.starttag() # to accept **attributes; current stub treats the 4th parameter # as empty: bool. self.body.append( self.starttag( node, tag_name, CLASS=f'admonition {name}', **attributes, # type: ignore[arg-type] ) ) self.context.append(f'\n') if name: node.insert(0, nodes.title(name, admonitionlabels[name])) def depart_admonition(self, node: Element | None = None) -> None: self.body.append(self.context.pop()) 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) -> tuple[int, ...] | None: if node.get('secnumber'): return node['secnumber'] if isinstance(node.parent, nodes.section): if self.builder.name == 'singlehtml': docname = self.docnames[-1] anchorname = f'{docname}/#{node.parent["ids"][0]}' if anchorname not in self.builder.secnumbers: # try first heading which has no anchor anchorname = f'{docname}/' 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( '%s' % ('.'.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 = f'{self.docnames[-1]}/{figtype}' else: key = figtype if figure_id in self.builder.fignumbers.get(key, {}): self.body.append('') 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('') figtype = self._domains.standard_domain.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: icon = self.config.html_permalinks_icon if node['ids'] and self.config.html_permalinks and self.builder.add_permalinks: self.body.append( f'{icon}', ) # overwritten def visit_bullet_list(self, node: nodes.bullet_list) -> None: if len(node) == 1 and isinstance(node[0], addnodes.toctree): # avoid emitting empty raise nodes.SkipNode super().visit_bullet_list(node) # overwritten def visit_definition(self, node: Element) -> None: # don't insert here. self.body.append(self.starttag(node, 'dd', '')) # overwritten def depart_definition(self, node: Element) -> None: self.body.append('\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('') next_node: Node | None = node.next_node(descend=False, siblings=True) if not isinstance(next_node, nodes.classifier): # close `
` tag at the tail of classifiers self.body.append('
') # 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 | None = 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, _('Link to this term')) self.body.append('') # overwritten def visit_title(self, node: nodes.title) -> None: if ( isinstance(node.parent, addnodes.compact_paragraph) and node.parent.get('toctree') ): # fmt: skip self.body.append( self.starttag(node, 'p', '', CLASS='caption', ROLE='heading') ) self.body.append('') self.context.append('

\n') elif ( isinstance(node.parent, nodes.Admonition) and isinstance(node.parent, nodes.Element) and 'collapsible' in node.parent ): self.body.append( self.starttag(node, 'summary', '', CLASS='admonition-title') ) self.context.append('\n') else: super().visit_title(node) self.add_secnumber(node) self.add_fignumber(node.parent) if isinstance(node.parent, nodes.table): self.body.append('') # Partially revert https://sourceforge.net/p/docutils/code/9562/ if ( isinstance(node.parent, nodes.topic) and self.settings.toc_backlinks and 'contents' in node.parent['classes'] and self.body[-1].startswith(' self.body.pop() self.context[-1] = '

\n' def depart_title(self, node: nodes.title) -> 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('
{}'.format( _('Link to this heading'), self.config.html_permalinks_icon ) ) elif isinstance(node.parent, nodes.table): self.body.append('
') self.add_permalink_ref(node.parent, _('Link to this table')) elif isinstance(node.parent, nodes.table): self.body.append('') super().depart_title(node) # overwritten def visit_rubric(self, node: nodes.rubric) -> None: if 'heading-level' in node: level = node['heading-level'] if level in {1, 2, 3, 4, 5, 6}: self.body.append(self.starttag(node, f'h{level}', '', CLASS='rubric')) else: logger.warning( __('unsupported rubric heading level: %s'), level, type='html', location=node, ) super().visit_rubric(node) else: super().visit_rubric(node) # overwritten def depart_rubric(self, node: nodes.rubric) -> None: if (level := node.get('heading-level')) in {1, 2, 3, 4, 5, 6}: self.body.append(f'\n') else: super().depart_rubric(node) # overwritten def visit_literal_block(self, node: nodes.literal_block) -> 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 + '\n') raise nodes.SkipNode def visit_caption(self, node: nodes.caption) -> None: if ( isinstance(node.parent, nodes.container) and node.parent.get('literal_block') ): # fmt: skip self.body.append('
') 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: nodes.caption) -> None: self.body.append('') # append permalink if available if ( isinstance(node.parent, nodes.container) and node.parent.get('literal_block') ): # fmt: skip self.add_permalink_ref(node.parent, _('Link to this code')) elif isinstance(node.parent, nodes.figure): self.add_permalink_ref(node.parent, _('Link to this image')) elif node.parent.get('toctree'): self.add_permalink_ref(node.parent.parent, _('Link to this toctree')) if ( isinstance(node.parent, nodes.container) and node.parent.get('literal_block') ): # fmt: skip self.body.append('
\n') else: super().depart_caption(node) def visit_doctest_block(self, node: Element) -> None: self.visit_literal_block(node) # type: ignore[arg-type] # overwritten to add the
(for XHTML compliance) def visit_block_quote(self, node: Element) -> None: self.body.append(self.starttag(node, 'blockquote') + '
') def depart_block_quote(self, node: Element) -> None: self.body.append('
\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() + '
') raise nodes.SkipNode def depart_literal(self, node: Element) -> None: if 'kbd' in node['classes']: self.body.append('') else: self.protect_literal_text -= 1 self.body.append('') def visit_productionlist(self, node: Element) -> None: self.body.append(self.starttag(node, 'pre')) def depart_productionlist(self, node: Element) -> None: self.body.append('
\n') 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') + '') def depart_centered(self, node: Element) -> None: self.body.append('

') 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'] # TODO: TYPING: Upstream docutils should type HTMLTranslator.starttag() # to accept **attributes; current stub treats the 4th parameter # as empty: bool. self.body.append(self.starttag(node, 'a', '', **atts)) # type: ignore[arg-type] self.context.append('
') elif 'filename' in node: atts['class'] += ' internal' atts['href'] = posixpath.join( self.builder.dlpath, urllib.parse.quote(node['filename']) ) # TODO: TYPING: Upstream docutils should type HTMLTranslator.starttag() # to accept **attributes; current stub treats the 4th parameter # as empty: bool. self.body.append(self.starttag(node, 'a', '', **atts)) # type: ignore[arg-type] self.context.append('') else: self.context.append('') def depart_download_reference(self, node: Element) -> None: self.body.append(self.context.pop()) # overwritten def visit_figure(self, node: nodes.figure) -> 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: nodes.image) -> 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(self.builder.srcdir / olduri) if size is None: logger.warning( __('Could not obtain image size. :scale: option is ignored.'), location=node, ) else: if 'width' not in node: node['width'] = str(size[0]) if 'height' not in node: node['height'] = str(size[1]) super().visit_image(node) # overwritten def depart_image(self, node: nodes.image) -> 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('') def depart_hlist(self, node: Element) -> None: self.body.append('
\n') def visit_hlistcol(self, node: Element) -> None: self.body.append('') def depart_hlistcol(self, node: Element) -> None: self.body.append('') # 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('%s' % 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(' ' * (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: nodes.emphasis) -> None: return self.visit_emphasis(node) def depart_literal_emphasis(self, node: nodes.emphasis) -> None: return self.depart_emphasis(node) def visit_literal_strong(self, node: nodes.strong) -> None: return self.visit_strong(node) def depart_literal_strong(self, node: nodes.strong) -> None: return self.depart_strong(node) def visit_abbreviation(self, node: Element) -> None: attrs = {} if explanation := node.get('explanation', ''): attrs['title'] = explanation # TODO: TYPING: Upstream docutils should type HTMLTranslator.starttag() # to accept **attributes; current stub treats the 4th parameter # as empty: bool. self.body.append(self.starttag(node, 'abbr', '', **attrs)) # type: ignore[arg-type] def depart_abbreviation(self, node: Element) -> None: self.body.append('') def visit_manpage(self, node: nodes.emphasis) -> None: self.visit_literal_emphasis(node) def depart_manpage(self, node: nodes.emphasis) -> None: self.depart_literal_emphasis(node) # overwritten to add even/odd classes def visit_table(self, node: nodes.table) -> None: self._table_row_indices.append(0) atts = {} classes = [cls.strip(' \t\n') for cls in self.settings.table_style.split(',')] classes.insert(0, 'docutils') # compat # set align-default if align not specified to give a default style classes.append('align-%s' % node.get('align', 'default')) if 'width' in node: atts['style'] = 'width: %s' % node['width'] # TODO: TYPING: Upstream docutils should type HTMLTranslator.starttag() # to accept **attributes; current stub treats the 4th parameter # as empty: bool. tag = self.starttag(node, 'table', CLASS=' '.join(classes), **atts) # type: ignore[arg-type] self.body.append(tag) def depart_table(self, node: nodes.table) -> 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[attr-defined] def visit_field_list(self, node: nodes.field_list) -> None: self._fieldlist_row_indices.append(0) return super().visit_field_list(node) def depart_field_list(self, node: nodes.field_list) -> 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') def visit_math(self, node: nodes.math, math_env: str = '') -> None: self._has_maths_elements = True # see validate_math_renderer name: str = self.builder.math_renderer_name # type: ignore[assignment] visit, _ = self.builder.env._registry.html_inline_math_renderers[name] visit(self, node) def depart_math(self, node: nodes.math, math_env: str = '') -> None: # see validate_math_renderer name: str = self.builder.math_renderer_name # type: ignore[assignment] _, depart = self.builder.env._registry.html_inline_math_renderers[name] if depart: depart(self, node) def visit_math_block(self, node: nodes.math_block, math_env: str = '') -> None: self._has_maths_elements = True # see validate_math_renderer name: str = self.builder.math_renderer_name # type: ignore[assignment] visit, _ = self.builder.env._registry.html_block_math_renderers[name] visit(self, node) def depart_math_block(self, node: nodes.math_block, math_env: str = '') -> None: # see validate_math_renderer name: str = self.builder.math_renderer_name # type: ignore[assignment] _, depart = self.builder.env._registry.html_block_math_renderers[name] if depart: depart(self, node) # See Docutils r9413 # Re-instate the footnote-reference class def visit_footnote_reference(self, node: Element) -> None: href = '#' + node['refid'] classes = ['footnote-reference', self.settings.footnote_references] self.body.append( self.starttag( node, 'a', suffix='', classes=classes, role='doc-noteref', href=href ) ) self.body.append('[')