From cf7da1843c45a4c2df7a749f7886a2d2ba0ee92a Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 15 Apr 2024 19:25:40 +0200 Subject: Adding upstream version 7.2.6. Signed-off-by: Daniel Baumann --- sphinx/writers/html5.py | 936 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 936 insertions(+) create mode 100644 sphinx/writers/html5.py (limited to 'sphinx/writers/html5.py') diff --git a/sphinx/writers/html5.py b/sphinx/writers/html5.py new file mode 100644 index 0000000..eb0a1ee --- /dev/null +++ b/sphinx/writers/html5.py @@ -0,0 +1,936 @@ +"""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: +# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html + + +def multiply_length(length: str, scale: int) -> str: + """Multiply *length* (width or height) by *scale*.""" + matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length) + if not matched: + return length + 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: # type: ignore[override] + 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('') + + 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')) + names = [] + productionlist = cast(Iterable[addnodes.production], node) + for production in productionlist: + names.append(production['tokenname']) + maxlen = max(len(name) for name in names) + lastname = None + for production in productionlist: + if production['tokenname']: + lastname = production['tokenname'].ljust(maxlen) + self.body.append(self.starttag(production, 'strong', '')) + self.body.append(lastname + ' ::= ') + 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) + if self.manpages_url: + node['refuri'] = self.manpages_url.format(**node.attributes) + self.visit_reference(node) + + def depart_manpage(self, node: Element) -> None: + if self.manpages_url: + self.depart_reference(node) + self.depart_literal_emphasis(node) + + # overwritten to add even/odd classes + + def visit_table(self, node: Element) -> None: + self._table_row_indices.append(0) + + 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): + 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('[') -- cgit v1.2.3