"""Experimental docutils writers for HTML5 handling Sphinx's custom nodes.""" from __future__ import annotations import os import posixpath import re import urllib.parse from collections.abc import Iterable from typing import TYPE_CHECKING, cast 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 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.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('' % 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) 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 (see issue #1001): # # foo([a, ]b, c[, d]) # 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: is_last_group = self.param_group_index + 1 == len(self.list_is_required_param) 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): 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 if self.multi_line_parameter_list: # If it's the first time we go down one level, add the separator # before the bracket. if self.optional_param_level == self.max_optional_param_level - 1: 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 self.optional_param_level == 0: self.body.append('\n') else: self.body.append(']') if self.optional_param_level == 0: self.param_group_index += 1 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('') ############################################## 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:'): 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'] 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: 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])) def depart_admonition(self, node: Element | None = None) -> None: self.body.append('\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) -> 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 = "{}/#{}".format(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('%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.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: 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: Element) -> 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 = 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 = 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: 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('') 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: 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('
{}'.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_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 + '\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('
') 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('') # append permalink if available if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'): 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'): self.body.append('
\n') else: super().depart_caption(node) def visit_doctest_block(self, node: Element) -> None: self.visit_literal_block(node) # 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')) productionlist = cast(Iterable[addnodes.production], node) names = (production['tokenname'] for production in productionlist) 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 + ' ::= ') elif lastname is not None: self.body.append('%s ' % (' ' * len(lastname))) production.walkabout(self) self.body.append('\n') self.body.append('\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") + '') 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'] self.body.append(self.starttag(node, 'a', '', **atts)) self.context.append('
') 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('') 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): path = os.path.join(self.builder.srcdir, olduri) # type: ignore[has-type] size = get_image_size(path) 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]) 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('') 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: 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('') def visit_manpage(self, node: Element) -> None: self.visit_literal_emphasis(node) def depart_manpage(self, node: Element) -> None: self.depart_literal_emphasis(node) # overwritten to add even/odd classes def visit_table(self, node: Element) -> 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'] tag = self.starttag(node, 'table', CLASS=' '.join(classes), **atts) self.body.append(tag) 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[attr-defined] 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') def visit_math(self, node: Element, math_env: str = '') -> None: # see validate_math_renderer name: str = self.builder.math_renderer_name # type: ignore[assignment] visit, _ = self.builder.app.registry.html_inline_math_renderers[name] visit(self, node) def depart_math(self, node: Element, math_env: str = '') -> None: # see validate_math_renderer name: str = self.builder.math_renderer_name # type: ignore[assignment] _, 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: # see validate_math_renderer name: str = self.builder.math_renderer_name # type: ignore[assignment] visit, _ = self.builder.app.registry.html_block_math_renderers[name] visit(self, node) def depart_math_block(self, node: Element, math_env: str = '') -> None: # see validate_math_renderer name: str = self.builder.math_renderer_name # type: ignore[assignment] _, depart = self.builder.app.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('[')