summaryrefslogtreecommitdiff
path: root/sphinx/writers
diff options
context:
space:
mode:
authorAdam Turner <9087854+AA-Turner@users.noreply.github.com>2022-12-30 00:53:04 +0000
committerGitHub <noreply@github.com>2022-12-30 00:53:04 +0000
commitbf06d7ef4d808c1e743267c38b5faed08cf3f21a (patch)
tree0d41419b5bd57b9124ad4fcf1619b23f3f7ed23c /sphinx/writers
parentaa2fa38feff75cf1f87755bf17417cd05c0be683 (diff)
downloadsphinx-git-bf06d7ef4d808c1e743267c38b5faed08cf3f21a.tar.gz
Factor out HTML 4 translator (#11051)
Move the HTML 4 translator into a private module.
Diffstat (limited to 'sphinx/writers')
-rw-r--r--sphinx/writers/_html4.py856
-rw-r--r--sphinx/writers/html.py851
2 files changed, 861 insertions, 846 deletions
diff --git a/sphinx/writers/_html4.py b/sphinx/writers/_html4.py
new file mode 100644
index 000000000..4ff2ebd3a
--- /dev/null
+++ b/sphinx/writers/_html4.py
@@ -0,0 +1,856 @@
+"""Frozen HTML 4 translator."""
+
+import os
+import posixpath
+import re
+import urllib.parse
+from typing import TYPE_CHECKING, Iterable, Optional, Tuple, cast
+
+from docutils import nodes
+from docutils.nodes import Element, Node, Text
+from docutils.writers.html4css1 import HTMLTranslator as BaseTranslator
+
+from sphinx import addnodes
+from sphinx.builders import Builder
+from sphinx.locale import _, __, admonitionlabels
+from sphinx.util import logging
+from sphinx.util.docutils import SphinxTranslator
+from sphinx.util.images import get_image_size
+
+if TYPE_CHECKING:
+ from sphinx.builders.html import StandaloneHTMLBuilder
+
+
+logger = logging.getLogger(__name__)
+
+
+def multiply_length(length: str, scale: int) -> str:
+ """Multiply *length* (width or height) by *scale*."""
+ matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length)
+ if not matched:
+ return length
+ elif scale == 100:
+ return length
+ else:
+ amount, unit = matched.groups()
+ result = float(amount) * scale / 100
+ return "%s%s" % (int(result), unit)
+
+
+# RemovedInSphinx70Warning
+class HTML4Translator(SphinxTranslator, BaseTranslator):
+ """
+ Our custom HTML translator.
+ """
+
+ builder: "StandaloneHTMLBuilder"
+
+ def __init__(self, document: nodes.document, builder: Builder) -> None:
+ super().__init__(document, builder)
+
+ self.highlighter = self.builder.highlighter
+ self.docnames = [self.builder.current_docname] # for singlehtml builder
+ self.manpages_url = self.config.manpages_url
+ self.protect_literal_text = 0
+ self.secnumber_suffix = self.config.html_secnumber_suffix
+ self.param_separator = ''
+ self.optional_param_level = 0
+ self._table_row_indices = [0]
+ self._fieldlist_row_indices = [0]
+ self.required_params_left = 0
+
+ def visit_start_of_file(self, node: Element) -> None:
+ # only occurs in the single-file builder
+ self.docnames.append(node['docname'])
+ self.body.append('<span id="document-%s"></span>' % node['docname'])
+
+ def depart_start_of_file(self, node: Element) -> None:
+ self.docnames.pop()
+
+ #############################################################
+ # Domain-specific object descriptions
+ #############################################################
+
+ # Top-level nodes for descriptions
+ ##################################
+
+ def visit_desc(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'dl'))
+
+ def depart_desc(self, node: Element) -> None:
+ self.body.append('</dl>\n\n')
+
+ def visit_desc_signature(self, node: Element) -> None:
+ # the id is set automatically
+ self.body.append(self.starttag(node, 'dt'))
+ self.protect_literal_text += 1
+
+ def depart_desc_signature(self, node: Element) -> None:
+ self.protect_literal_text -= 1
+ if not node.get('is_multiline'):
+ self.add_permalink_ref(node, _('Permalink to this definition'))
+ self.body.append('</dt>\n')
+
+ def visit_desc_signature_line(self, node: Element) -> None:
+ pass
+
+ def depart_desc_signature_line(self, node: Element) -> None:
+ if node.get('add_permalink'):
+ # the permalink info is on the parent desc_signature node
+ self.add_permalink_ref(node.parent, _('Permalink to this definition'))
+ self.body.append('<br />')
+
+ def visit_desc_content(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'dd', ''))
+
+ def depart_desc_content(self, node: Element) -> None:
+ self.body.append('</dd>')
+
+ def visit_desc_inline(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'span', ''))
+
+ def depart_desc_inline(self, node: Element) -> None:
+ self.body.append('</span>')
+
+ # Nodes for high-level structure in signatures
+ ##############################################
+
+ def visit_desc_name(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'code', ''))
+
+ def depart_desc_name(self, node: Element) -> None:
+ self.body.append('</code>')
+
+ def visit_desc_addname(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'code', ''))
+
+ def depart_desc_addname(self, node: Element) -> None:
+ self.body.append('</code>')
+
+ def visit_desc_type(self, node: Element) -> None:
+ pass
+
+ def depart_desc_type(self, node: Element) -> None:
+ pass
+
+ def visit_desc_returns(self, node: Element) -> None:
+ self.body.append(' <span class="sig-return">')
+ self.body.append('<span class="sig-return-icon">&#x2192;</span>')
+ self.body.append(' <span class="sig-return-typehint">')
+
+ def depart_desc_returns(self, node: Element) -> None:
+ self.body.append('</span></span>')
+
+ def visit_desc_parameterlist(self, node: Element) -> None:
+ self.body.append('<span class="sig-paren">(</span>')
+ self.first_param = 1
+ self.optional_param_level = 0
+ # How many required parameters are left.
+ self.required_params_left = sum([isinstance(c, addnodes.desc_parameter)
+ for c in node.children])
+ self.param_separator = node.child_text_separator
+
+ def depart_desc_parameterlist(self, node: Element) -> None:
+ self.body.append('<span class="sig-paren">)</span>')
+
+ # If required parameters are still to come, then put the comma after
+ # the parameter. Otherwise, put the comma before. This ensures that
+ # signatures like the following render correctly (see issue #1001):
+ #
+ # foo([a, ]b, c[, d])
+ #
+ def visit_desc_parameter(self, node: Element) -> None:
+ if self.first_param:
+ self.first_param = 0
+ elif not self.required_params_left:
+ self.body.append(self.param_separator)
+ if self.optional_param_level == 0:
+ self.required_params_left -= 1
+ if not node.hasattr('noemph'):
+ self.body.append('<em>')
+
+ def depart_desc_parameter(self, node: Element) -> None:
+ if not node.hasattr('noemph'):
+ self.body.append('</em>')
+ if self.required_params_left:
+ self.body.append(self.param_separator)
+
+ def visit_desc_optional(self, node: Element) -> None:
+ self.optional_param_level += 1
+ self.body.append('<span class="optional">[</span>')
+
+ def depart_desc_optional(self, node: Element) -> None:
+ self.optional_param_level -= 1
+ self.body.append('<span class="optional">]</span>')
+
+ def visit_desc_annotation(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'em', '', CLASS='property'))
+
+ def depart_desc_annotation(self, node: Element) -> None:
+ self.body.append('</em>')
+
+ ##############################################
+
+ def visit_versionmodified(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'div', CLASS=node['type']))
+
+ def depart_versionmodified(self, node: Element) -> None:
+ self.body.append('</div>\n')
+
+ # overwritten
+ def visit_reference(self, node: Element) -> None:
+ atts = {'class': 'reference'}
+ if node.get('internal') or 'refuri' not in node:
+ atts['class'] += ' internal'
+ else:
+ atts['class'] += ' external'
+ if 'refuri' in node:
+ atts['href'] = node['refuri'] or '#'
+ if self.settings.cloak_email_addresses and atts['href'].startswith('mailto:'):
+ atts['href'] = self.cloak_mailto(atts['href'])
+ self.in_mailto = True
+ else:
+ assert 'refid' in node, \
+ 'References must have "refuri" or "refid" attribute.'
+ atts['href'] = '#' + node['refid']
+ if not isinstance(node.parent, nodes.TextElement):
+ assert len(node) == 1 and isinstance(node[0], nodes.image)
+ atts['class'] += ' image-reference'
+ if 'reftitle' in node:
+ atts['title'] = node['reftitle']
+ if 'target' in node:
+ atts['target'] = node['target']
+ self.body.append(self.starttag(node, 'a', '', **atts))
+
+ if node.get('secnumber'):
+ self.body.append(('%s' + self.secnumber_suffix) %
+ '.'.join(map(str, node['secnumber'])))
+
+ def visit_number_reference(self, node: Element) -> None:
+ self.visit_reference(node)
+
+ def depart_number_reference(self, node: Element) -> None:
+ self.depart_reference(node)
+
+ # overwritten -- we don't want source comments to show up in the HTML
+ def visit_comment(self, node: Element) -> None: # type: ignore
+ raise nodes.SkipNode
+
+ # overwritten
+ def visit_admonition(self, node: Element, name: str = '') -> None:
+ self.body.append(self.starttag(
+ node, 'div', CLASS=('admonition ' + name)))
+ if name:
+ node.insert(0, nodes.title(name, admonitionlabels[name]))
+ self.set_first_last(node)
+
+ def depart_admonition(self, node: Optional[Element] = None) -> None:
+ self.body.append('</div>\n')
+
+ def visit_seealso(self, node: Element) -> None:
+ self.visit_admonition(node, 'seealso')
+
+ def depart_seealso(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def get_secnumber(self, node: Element) -> Optional[Tuple[int, ...]]:
+ if node.get('secnumber'):
+ return node['secnumber']
+ elif isinstance(node.parent, nodes.section):
+ if self.builder.name == 'singlehtml':
+ docname = self.docnames[-1]
+ anchorname = "%s/#%s" % (docname, node.parent['ids'][0])
+ if anchorname not in self.builder.secnumbers:
+ anchorname = "%s/" % docname # try first heading which has no anchor
+ else:
+ anchorname = '#' + node.parent['ids'][0]
+ if anchorname not in self.builder.secnumbers:
+ anchorname = '' # try first heading which has no anchor
+
+ if self.builder.secnumbers.get(anchorname):
+ return self.builder.secnumbers[anchorname]
+
+ return None
+
+ def add_secnumber(self, node: Element) -> None:
+ secnumber = self.get_secnumber(node)
+ if secnumber:
+ self.body.append('<span class="section-number">%s</span>' %
+ ('.'.join(map(str, secnumber)) + self.secnumber_suffix))
+
+ def add_fignumber(self, node: Element) -> None:
+ def append_fignumber(figtype: str, figure_id: str) -> None:
+ if self.builder.name == 'singlehtml':
+ key = "%s/%s" % (self.docnames[-1], figtype)
+ else:
+ key = figtype
+
+ if figure_id in self.builder.fignumbers.get(key, {}):
+ self.body.append('<span class="caption-number">')
+ prefix = self.config.numfig_format.get(figtype)
+ if prefix is None:
+ msg = __('numfig_format is not defined for %s') % figtype
+ logger.warning(msg)
+ else:
+ numbers = self.builder.fignumbers[key][figure_id]
+ self.body.append(prefix % '.'.join(map(str, numbers)) + ' ')
+ self.body.append('</span>')
+
+ figtype = self.builder.env.domains['std'].get_enumerable_node_type(node)
+ if figtype:
+ if len(node['ids']) == 0:
+ msg = __('Any IDs not assigned for %s node') % node.tagname
+ logger.warning(msg, location=node)
+ else:
+ append_fignumber(figtype, node['ids'][0])
+
+ def add_permalink_ref(self, node: Element, title: str) -> None:
+ if node['ids'] and self.config.html_permalinks and self.builder.add_permalinks:
+ format = '<a class="headerlink" href="#%s" title="%s">%s</a>'
+ self.body.append(format % (node['ids'][0], title,
+ self.config.html_permalinks_icon))
+
+ def generate_targets_for_listing(self, node: Element) -> None:
+ """Generate hyperlink targets for listings.
+
+ Original visit_bullet_list(), visit_definition_list() and visit_enumerated_list()
+ generates hyperlink targets inside listing tags (<ul>, <ol> and <dl>) if multiple
+ IDs are assigned to listings. That is invalid DOM structure.
+ (This is a bug of docutils <= 0.12)
+
+ This exports hyperlink targets before listings to make valid DOM structure.
+ """
+ for id in node['ids'][1:]:
+ self.body.append('<span id="%s"></span>' % id)
+ node['ids'].remove(id)
+
+ # overwritten
+ def visit_bullet_list(self, node: Element) -> None:
+ if len(node) == 1 and isinstance(node[0], addnodes.toctree):
+ # avoid emitting empty <ul></ul>
+ raise nodes.SkipNode
+ self.generate_targets_for_listing(node)
+ super().visit_bullet_list(node)
+
+ # overwritten
+ def visit_enumerated_list(self, node: Element) -> None:
+ self.generate_targets_for_listing(node)
+ super().visit_enumerated_list(node)
+
+ # overwritten
+ def visit_definition(self, node: Element) -> None:
+ # don't insert </dt> here.
+ self.body.append(self.starttag(node, 'dd', ''))
+
+ # overwritten
+ def depart_definition(self, node: Element) -> None:
+ self.body.append('</dd>\n')
+
+ # overwritten
+ def visit_classifier(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
+
+ # overwritten
+ def depart_classifier(self, node: Element) -> None:
+ self.body.append('</span>')
+
+ next_node: Node = node.next_node(descend=False, siblings=True)
+ if not isinstance(next_node, nodes.classifier):
+ # close `<dt>` tag at the tail of classifiers
+ self.body.append('</dt>')
+
+ # overwritten
+ def visit_term(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'dt', ''))
+
+ # overwritten
+ def depart_term(self, node: Element) -> None:
+ next_node: Node = node.next_node(descend=False, siblings=True)
+ if isinstance(next_node, nodes.classifier):
+ # Leave the end tag to `self.depart_classifier()`, in case
+ # there's a classifier.
+ pass
+ else:
+ if isinstance(node.parent.parent.parent, addnodes.glossary):
+ # add permalink if glossary terms
+ self.add_permalink_ref(node, _('Permalink to this term'))
+
+ self.body.append('</dt>')
+
+ # overwritten
+ def visit_title(self, node: Element) -> None:
+ if isinstance(node.parent, addnodes.compact_paragraph) and node.parent.get('toctree'):
+ self.body.append(self.starttag(node, 'p', '', CLASS='caption', ROLE='heading'))
+ self.body.append('<span class="caption-text">')
+ self.context.append('</span></p>\n')
+ else:
+ super().visit_title(node)
+ self.add_secnumber(node)
+ self.add_fignumber(node.parent)
+ if isinstance(node.parent, nodes.table):
+ self.body.append('<span class="caption-text">')
+
+ def depart_title(self, node: Element) -> None:
+ close_tag = self.context[-1]
+ if (self.config.html_permalinks and self.builder.add_permalinks and
+ node.parent.hasattr('ids') and node.parent['ids']):
+ # add permalink anchor
+ if close_tag.startswith('</h'):
+ self.add_permalink_ref(node.parent, _('Permalink to this heading'))
+ elif close_tag.startswith('</a></h'):
+ self.body.append('</a><a class="headerlink" href="#%s" ' %
+ node.parent['ids'][0] +
+ 'title="%s">%s' % (
+ _('Permalink to this heading'),
+ self.config.html_permalinks_icon))
+ elif isinstance(node.parent, nodes.table):
+ self.body.append('</span>')
+ self.add_permalink_ref(node.parent, _('Permalink to this table'))
+ elif isinstance(node.parent, nodes.table):
+ self.body.append('</span>')
+
+ super().depart_title(node)
+
+ # overwritten
+ def visit_literal_block(self, node: Element) -> None:
+ if node.rawsource != node.astext():
+ # most probably a parsed-literal block -- don't highlight
+ return super().visit_literal_block(node)
+
+ lang = node.get('language', 'default')
+ linenos = node.get('linenos', False)
+ highlight_args = node.get('highlight_args', {})
+ highlight_args['force'] = node.get('force', False)
+ opts = self.config.highlight_options.get(lang, {})
+
+ if linenos and self.config.html_codeblock_linenos_style:
+ linenos = self.config.html_codeblock_linenos_style
+
+ highlighted = self.highlighter.highlight_block(
+ node.rawsource, lang, opts=opts, linenos=linenos,
+ location=node, **highlight_args
+ )
+ starttag = self.starttag(node, 'div', suffix='',
+ CLASS='highlight-%s notranslate' % lang)
+ self.body.append(starttag + highlighted + '</div>\n')
+ raise nodes.SkipNode
+
+ def visit_caption(self, node: Element) -> None:
+ if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
+ self.body.append('<div class="code-block-caption">')
+ else:
+ super().visit_caption(node)
+ self.add_fignumber(node.parent)
+ self.body.append(self.starttag(node, 'span', '', CLASS='caption-text'))
+
+ def depart_caption(self, node: Element) -> None:
+ self.body.append('</span>')
+
+ # append permalink if available
+ if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
+ self.add_permalink_ref(node.parent, _('Permalink to this code'))
+ elif isinstance(node.parent, nodes.figure):
+ self.add_permalink_ref(node.parent, _('Permalink to this image'))
+ elif node.parent.get('toctree'):
+ self.add_permalink_ref(node.parent.parent, _('Permalink to this toctree'))
+
+ if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
+ self.body.append('</div>\n')
+ else:
+ super().depart_caption(node)
+
+ def visit_doctest_block(self, node: Element) -> None:
+ self.visit_literal_block(node)
+
+ # overwritten to add the <div> (for XHTML compliance)
+ def visit_block_quote(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'blockquote') + '<div>')
+
+ def depart_block_quote(self, node: Element) -> None:
+ self.body.append('</div></blockquote>\n')
+
+ # overwritten
+ def visit_literal(self, node: Element) -> None:
+ if 'kbd' in node['classes']:
+ self.body.append(self.starttag(node, 'kbd', '',
+ CLASS='docutils literal notranslate'))
+ return
+ lang = node.get("language", None)
+ if 'code' not in node['classes'] or not lang:
+ self.body.append(self.starttag(node, 'code', '',
+ CLASS='docutils literal notranslate'))
+ self.protect_literal_text += 1
+ return
+
+ opts = self.config.highlight_options.get(lang, {})
+ highlighted = self.highlighter.highlight_block(
+ node.astext(), lang, opts=opts, location=node, nowrap=True)
+ starttag = self.starttag(
+ node,
+ "code",
+ suffix="",
+ CLASS="docutils literal highlight highlight-%s" % lang,
+ )
+ self.body.append(starttag + highlighted.strip() + "</code>")
+ raise nodes.SkipNode
+
+ def depart_literal(self, node: Element) -> None:
+ if 'kbd' in node['classes']:
+ self.body.append('</kbd>')
+ else:
+ self.protect_literal_text -= 1
+ self.body.append('</code>')
+
+ def visit_productionlist(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'pre'))
+ names = []
+ productionlist = cast(Iterable[addnodes.production], node)
+ for production in productionlist:
+ names.append(production['tokenname'])
+ maxlen = max(len(name) for name in names)
+ lastname = None
+ for production in productionlist:
+ if production['tokenname']:
+ lastname = production['tokenname'].ljust(maxlen)
+ self.body.append(self.starttag(production, 'strong', ''))
+ self.body.append(lastname + '</strong> ::= ')
+ elif lastname is not None:
+ self.body.append('%s ' % (' ' * len(lastname)))
+ production.walkabout(self)
+ self.body.append('\n')
+ self.body.append('</pre>\n')
+ raise nodes.SkipNode
+
+ def depart_productionlist(self, node: Element) -> None:
+ pass
+
+ def visit_production(self, node: Element) -> None:
+ pass
+
+ def depart_production(self, node: Element) -> None:
+ pass
+
+ def visit_centered(self, node: Element) -> None:
+ self.body.append(self.starttag(node, 'p', CLASS="centered") +
+ '<strong>')
+
+ def depart_centered(self, node: Element) -> None:
+ self.body.append('</strong></p>')
+
+ # overwritten
+ def should_be_compact_paragraph(self, node: Node) -> bool:
+ """Determine if the <p> tags around paragraph can be omitted."""
+ if isinstance(node.parent, addnodes.desc_content):
+ # Never compact desc_content items.
+ return False
+ if isinstance(node.parent, addnodes.versionmodified):
+ # Never compact versionmodified nodes.
+ return False
+ return super().should_be_compact_paragraph(node)
+
+ def visit_compact_paragraph(self, node: Element) -> None:
+ pass
+
+ def depart_compact_paragraph(self, node: Element) -> None:
+ pass
+
+ def visit_download_reference(self, node: Element) -> None:
+ atts = {'class': 'reference download',
+ 'download': ''}
+
+ if not self.builder.download_support:
+ self.context.append('')
+ elif 'refuri' in node:
+ atts['class'] += ' external'
+ atts['href'] = node['refuri']
+ self.body.append(self.starttag(node, 'a', '', **atts))
+ self.context.append('</a>')
+ elif 'filename' in node:
+ atts['class'] += ' internal'
+ atts['href'] = posixpath.join(self.builder.dlpath,
+ urllib.parse.quote(node['filename']))
+ self.body.append(self.starttag(node, 'a', '', **atts))
+ self.context.append('</a>')
+ else:
+ self.context.append('')
+
+ def depart_download_reference(self, node: Element) -> None:
+ self.body.append(self.context.pop())
+
+ # overwritten
+ def visit_figure(self, node: Element) -> None:
+ # set align=default if align not specified to give a default style
+ node.setdefault('align', 'default')
+
+ return super().visit_figure(node)
+
+ # overwritten
+ def visit_image(self, node: Element) -> None:
+ olduri = node['uri']
+ # rewrite the URI if the environment knows about it
+ if olduri in self.builder.images:
+ node['uri'] = posixpath.join(self.builder.imgpath,
+ urllib.parse.quote(self.builder.images[olduri]))
+
+ if 'scale' in node:
+ # Try to figure out image height and width. Docutils does that too,
+ # but it tries the final file name, which does not necessarily exist
+ # yet at the time the HTML file is written.
+ if not ('width' in node and 'height' in node):
+ size = get_image_size(os.path.join(self.builder.srcdir, olduri))
+ if size is None:
+ logger.warning(
+ __('Could not obtain image size. :scale: option is ignored.'),
+ location=node,
+ )
+ else:
+ if 'width' not in node:
+ node['width'] = str(size[0])
+ if 'height' not in node:
+ node['height'] = str(size[1])
+
+ uri = node['uri']
+ if uri.lower().endswith(('svg', 'svgz')):
+ atts = {'src': uri}
+ if 'width' in node:
+ atts['width'] = node['width']
+ if 'height' in node:
+ atts['height'] = node['height']
+ if 'scale' in node:
+ if 'width' in atts:
+ atts['width'] = multiply_length(atts['width'], node['scale'])
+ if 'height' in atts:
+ atts['height'] = multiply_length(atts['height'], node['scale'])
+ atts['alt'] = node.get('alt', uri)
+ if 'align' in node:
+ atts['class'] = 'align-%s' % node['align']
+ self.body.append(self.emptytag(node, 'img', '', **atts))
+ return
+
+ super().visit_image(node)
+
+ # overwritten
+ def depart_image(self, node: Element) -> None:
+ if node['uri'].lower().endswith(('svg', 'svgz')):
+ pass
+ else:
+ super().depart_image(node)
+
+ def visit_toctree(self, node: Element) -> None:
+ # this only happens when formatting a toc from env.tocs -- in this
+ # case we don't want to include the subtree
+ raise nodes.SkipNode
+
+ def visit_index(self, node: Element) -> None:
+ raise nodes.SkipNode
+
+ def visit_tabular_col_spec(self, node: Element) -> None:
+ raise nodes.SkipNode
+
+ def visit_glossary(self, node: Element) -> None:
+ pass
+
+ def depart_glossary(self, node: Element) -> None:
+ pass
+
+ def visit_acks(self, node: Element) -> None:
+ pass
+
+ def depart_acks(self, node: Element) -> None:
+ pass
+
+ def visit_hlist(self, node: Element) -> None:
+ self.body.append('<table class="hlist"><tr>')
+
+ def depart_hlist(self, node: Element) -> None:
+ self.body.append('</tr></table>\n')
+
+ def visit_hlistcol(self, node: Element) -> None:
+ self.body.append('<td>')
+
+ def depart_hlistcol(self, node: Element) -> None:
+ self.body.append('</td>')
+
+ def visit_option_group(self, node: Element) -> None:
+ super().visit_option_group(node)
+ self.context[-2] = self.context[-2].replace('&nbsp;', '&#160;')
+
+ # overwritten
+ def visit_Text(self, node: Text) -> None:
+ text = node.astext()
+ encoded = self.encode(text)
+ if self.protect_literal_text:
+ # moved here from base class's visit_literal to support
+ # more formatting in literal nodes
+ for token in self.words_and_spaces.findall(encoded):
+ if token.strip():
+ # protect literal text from line wrapping
+ self.body.append('<span class="pre">%s</span>' % token)
+ elif token in ' \n':
+ # allow breaks at whitespace
+ self.body.append(token)
+ else:
+ # protect runs of multiple spaces; the last one can wrap
+ self.body.append('&#160;' * (len(token) - 1) + ' ')
+ else:
+ if self.in_mailto and self.settings.cloak_email_addresses:
+ encoded = self.cloak_email(encoded)
+ self.body.append(encoded)
+
+ def visit_note(self, node: Element) -> None:
+ self.visit_admonition(node, 'note')
+
+ def depart_note(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def visit_warning(self, node: Element) -> None:
+ self.visit_admonition(node, 'warning')
+
+ def depart_warning(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def visit_attention(self, node: Element) -> None:
+ self.visit_admonition(node, 'attention')
+
+ def depart_attention(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def visit_caution(self, node: Element) -> None:
+ self.visit_admonition(node, 'caution')
+
+ def depart_caution(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def visit_danger(self, node: Element) -> None:
+ self.visit_admonition(node, 'danger')
+
+ def depart_danger(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def visit_error(self, node: Element) -> None:
+ self.visit_admonition(node, 'error')
+
+ def depart_error(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def visit_hint(self, node: Element) -> None:
+ self.visit_admonition(node, 'hint')
+
+ def depart_hint(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def visit_important(self, node: Element) -> None:
+ self.visit_admonition(node, 'important')
+
+ def depart_important(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def visit_tip(self, node: Element) -> None:
+ self.visit_admonition(node, 'tip')
+
+ def depart_tip(self, node: Element) -> None:
+ self.depart_admonition(node)
+
+ def visit_literal_emphasis(self, node: Element) -> None:
+ return self.visit_emphasis(node)
+
+ def depart_literal_emphasis(self, node: Element) -> None:
+ return self.depart_emphasis(node)
+
+ def visit_literal_strong(self, node: Element) -> None:
+ return self.visit_strong(node)
+
+ def depart_literal_strong(self, node: Element) -> None:
+ return self.depart_strong(node)
+
+ def visit_abbreviation(self, node: Element) -> None:
+ attrs = {}
+ if node.hasattr('explanation'):
+ attrs['title'] = node['explanation']
+ self.body.append(self.starttag(node, 'abbr', '', **attrs))
+
+ def depart_abbreviation(self, node: Element) -> None:
+ self.body.append('</abbr>')
+
+ def visit_manpage(self, node: Element) -> None:
+ self.visit_literal_emphasis(node)
+ if self.manpages_url:
+ node['refuri'] = self.manpages_url.format(**node.attributes)
+ self.visit_reference(node)
+
+ def depart_manpage(self, node: Element) -> None:
+ if self.manpages_url:
+ self.depart_reference(node)
+ self.depart_literal_emphasis(node)
+
+ # overwritten to add even/odd classes
+
+ def visit_table(self, node: Element) -> None:
+ self._table_row_indices.append(0)
+
+ # set align=default if align not specified to give a default style
+ node.setdefault('align', 'default')
+
+ return super().visit_table(node)
+
+ def depart_table(self, node: Element) -> None:
+ self._table_row_indices.pop()
+ super().depart_table(node)
+
+ def visit_row(self, node: Element) -> None:
+ self._table_row_indices[-1] += 1
+ if self._table_row_indices[-1] % 2 == 0:
+ node['classes'].append('row-even')
+ else:
+ node['classes'].append('row-odd')
+ self.body.append(self.starttag(node, 'tr', ''))
+ node.column = 0 # type: ignore
+
+ def visit_entry(self, node: Element) -> None:
+ super().visit_entry(node)
+ if self.body[-1] == '&nbsp;':
+ self.body[-1] = '&#160;'
+
+ def visit_field_list(self, node: Element) -> None:
+ self._fieldlist_row_indices.append(0)
+ return super().visit_field_list(node)
+
+ def depart_field_list(self, node: Element) -> None:
+ self._fieldlist_row_indices.pop()
+ return super().depart_field_list(node)
+
+ def visit_field(self, node: Element) -> None:
+ self._fieldlist_row_indices[-1] += 1
+ if self._fieldlist_row_indices[-1] % 2 == 0:
+ node['classes'].append('field-even')
+ else:
+ node['classes'].append('field-odd')
+ self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
+
+ def visit_field_name(self, node: Element) -> None:
+ context_count = len(self.context)
+ super().visit_field_name(node)
+ if context_count != len(self.context):
+ self.context[-1] = self.context[-1].replace('&nbsp;', '&#160;')
+
+ def visit_math(self, node: Element, math_env: str = '') -> None:
+ name = self.builder.math_renderer_name
+ visit, _ = self.builder.app.registry.html_inline_math_renderers[name]
+ visit(self, node)
+
+ def depart_math(self, node: Element, math_env: str = '') -> None:
+ name = self.builder.math_renderer_name
+ _, depart = self.builder.app.registry.html_inline_math_renderers[name]
+ if depart: # type: ignore[truthy-function]
+ depart(self, node)
+
+ def visit_math_block(self, node: Element, math_env: str = '') -> None:
+ name = self.builder.math_renderer_name
+ visit, _ = self.builder.app.registry.html_block_math_renderers[name]
+ visit(self, node)
+
+ def depart_math_block(self, node: Element, math_env: str = '') -> None:
+ name = self.builder.math_renderer_name
+ _, depart = self.builder.app.registry.html_block_math_renderers[name]
+ if depart: # type: ignore[truthy-function]
+ depart(self, node)
diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py
index 938b6e77b..d72433b88 100644
--- a/sphinx/writers/html.py
+++ b/sphinx/writers/html.py
@@ -1,46 +1,24 @@
"""docutils writers handling Sphinx' custom nodes."""
-import os
-import posixpath
-import re
-import urllib.parse
-from typing import TYPE_CHECKING, Iterable, Optional, Tuple, cast
+from typing import TYPE_CHECKING, cast
-from docutils import nodes
-from docutils.nodes import Element, Node, Text
-from docutils.writers.html4css1 import HTMLTranslator as BaseTranslator
from docutils.writers.html4css1 import Writer
-from sphinx import addnodes
-from sphinx.builders import Builder
-from sphinx.locale import _, __, admonitionlabels
from sphinx.util import logging
-from sphinx.util.docutils import SphinxTranslator
-from sphinx.util.images import get_image_size
+from sphinx.writers._html4 import HTML4Translator
+from sphinx.writers.html5 import HTML5Translator # NoQA: F401
if TYPE_CHECKING:
from sphinx.builders.html import StandaloneHTMLBuilder
logger = logging.getLogger(__name__)
+HTMLTranslator = HTML4Translator
# A good overview of the purpose behind these classes can be found here:
# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html
-def multiply_length(length: str, scale: int) -> str:
- """Multiply *length* (width or height) by *scale*."""
- matched = re.match(r'^(\d*\.?\d*)\s*(\S*)$', length)
- if not matched:
- return length
- elif scale == 100:
- return length
- else:
- amount, unit = matched.groups()
- result = float(amount) * scale / 100
- return "%s%s" % (int(result), unit)
-
-
class HTMLWriter(Writer):
# override embed-stylesheet default value to False.
@@ -53,7 +31,7 @@ class HTMLWriter(Writer):
def translate(self) -> None:
# sadly, this is mostly copied from parent class
visitor = self.builder.create_translator(self.document, self.builder)
- self.visitor = cast(HTMLTranslator, visitor)
+ self.visitor = cast(HTML4Translator, visitor)
self.document.walkabout(visitor)
self.output = self.visitor.astext()
for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix',
@@ -63,822 +41,3 @@ class HTMLWriter(Writer):
'html_subtitle', 'html_body', ):
setattr(self, attr, getattr(visitor, attr, None))
self.clean_meta = ''.join(self.visitor.meta[2:])
-
-
-# RemovedInSphinx70Warning
-class HTMLTranslator(SphinxTranslator, BaseTranslator):
- """
- Our custom HTML translator.
- """
-
- builder: "StandaloneHTMLBuilder"
-
- def __init__(self, document: nodes.document, builder: Builder) -> None:
- super().__init__(document, builder)
-
- self.highlighter = self.builder.highlighter
- self.docnames = [self.builder.current_docname] # for singlehtml builder
- self.manpages_url = self.config.manpages_url
- self.protect_literal_text = 0
- self.secnumber_suffix = self.config.html_secnumber_suffix
- self.param_separator = ''
- self.optional_param_level = 0
- self._table_row_indices = [0]
- self._fieldlist_row_indices = [0]
- self.required_params_left = 0
-
- def visit_start_of_file(self, node: Element) -> None:
- # only occurs in the single-file builder
- self.docnames.append(node['docname'])
- self.body.append('<span id="document-%s"></span>' % node['docname'])
-
- def depart_start_of_file(self, node: Element) -> None:
- self.docnames.pop()
-
- #############################################################
- # Domain-specific object descriptions
- #############################################################
-
- # Top-level nodes for descriptions
- ##################################
-
- def visit_desc(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'dl'))
-
- def depart_desc(self, node: Element) -> None:
- self.body.append('</dl>\n\n')
-
- def visit_desc_signature(self, node: Element) -> None:
- # the id is set automatically
- self.body.append(self.starttag(node, 'dt'))
- self.protect_literal_text += 1
-
- def depart_desc_signature(self, node: Element) -> None:
- self.protect_literal_text -= 1
- if not node.get('is_multiline'):
- self.add_permalink_ref(node, _('Permalink to this definition'))
- self.body.append('</dt>\n')
-
- def visit_desc_signature_line(self, node: Element) -> None:
- pass
-
- def depart_desc_signature_line(self, node: Element) -> None:
- if node.get('add_permalink'):
- # the permalink info is on the parent desc_signature node
- self.add_permalink_ref(node.parent, _('Permalink to this definition'))
- self.body.append('<br />')
-
- def visit_desc_content(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'dd', ''))
-
- def depart_desc_content(self, node: Element) -> None:
- self.body.append('</dd>')
-
- def visit_desc_inline(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'span', ''))
-
- def depart_desc_inline(self, node: Element) -> None:
- self.body.append('</span>')
-
- # Nodes for high-level structure in signatures
- ##############################################
-
- def visit_desc_name(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'code', ''))
-
- def depart_desc_name(self, node: Element) -> None:
- self.body.append('</code>')
-
- def visit_desc_addname(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'code', ''))
-
- def depart_desc_addname(self, node: Element) -> None:
- self.body.append('</code>')
-
- def visit_desc_type(self, node: Element) -> None:
- pass
-
- def depart_desc_type(self, node: Element) -> None:
- pass
-
- def visit_desc_returns(self, node: Element) -> None:
- self.body.append(' <span class="sig-return">')
- self.body.append('<span class="sig-return-icon">&#x2192;</span>')
- self.body.append(' <span class="sig-return-typehint">')
-
- def depart_desc_returns(self, node: Element) -> None:
- self.body.append('</span></span>')
-
- def visit_desc_parameterlist(self, node: Element) -> None:
- self.body.append('<span class="sig-paren">(</span>')
- self.first_param = 1
- self.optional_param_level = 0
- # How many required parameters are left.
- self.required_params_left = sum([isinstance(c, addnodes.desc_parameter)
- for c in node.children])
- self.param_separator = node.child_text_separator
-
- def depart_desc_parameterlist(self, node: Element) -> None:
- self.body.append('<span class="sig-paren">)</span>')
-
- # If required parameters are still to come, then put the comma after
- # the parameter. Otherwise, put the comma before. This ensures that
- # signatures like the following render correctly (see issue #1001):
- #
- # foo([a, ]b, c[, d])
- #
- def visit_desc_parameter(self, node: Element) -> None:
- if self.first_param:
- self.first_param = 0
- elif not self.required_params_left:
- self.body.append(self.param_separator)
- if self.optional_param_level == 0:
- self.required_params_left -= 1
- if not node.hasattr('noemph'):
- self.body.append('<em>')
-
- def depart_desc_parameter(self, node: Element) -> None:
- if not node.hasattr('noemph'):
- self.body.append('</em>')
- if self.required_params_left:
- self.body.append(self.param_separator)
-
- def visit_desc_optional(self, node: Element) -> None:
- self.optional_param_level += 1
- self.body.append('<span class="optional">[</span>')
-
- def depart_desc_optional(self, node: Element) -> None:
- self.optional_param_level -= 1
- self.body.append('<span class="optional">]</span>')
-
- def visit_desc_annotation(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'em', '', CLASS='property'))
-
- def depart_desc_annotation(self, node: Element) -> None:
- self.body.append('</em>')
-
- ##############################################
-
- def visit_versionmodified(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'div', CLASS=node['type']))
-
- def depart_versionmodified(self, node: Element) -> None:
- self.body.append('</div>\n')
-
- # overwritten
- def visit_reference(self, node: Element) -> None:
- atts = {'class': 'reference'}
- if node.get('internal') or 'refuri' not in node:
- atts['class'] += ' internal'
- else:
- atts['class'] += ' external'
- if 'refuri' in node:
- atts['href'] = node['refuri'] or '#'
- if self.settings.cloak_email_addresses and atts['href'].startswith('mailto:'):
- atts['href'] = self.cloak_mailto(atts['href'])
- self.in_mailto = True
- else:
- assert 'refid' in node, \
- 'References must have "refuri" or "refid" attribute.'
- atts['href'] = '#' + node['refid']
- if not isinstance(node.parent, nodes.TextElement):
- assert len(node) == 1 and isinstance(node[0], nodes.image)
- atts['class'] += ' image-reference'
- if 'reftitle' in node:
- atts['title'] = node['reftitle']
- if 'target' in node:
- atts['target'] = node['target']
- self.body.append(self.starttag(node, 'a', '', **atts))
-
- if node.get('secnumber'):
- self.body.append(('%s' + self.secnumber_suffix) %
- '.'.join(map(str, node['secnumber'])))
-
- def visit_number_reference(self, node: Element) -> None:
- self.visit_reference(node)
-
- def depart_number_reference(self, node: Element) -> None:
- self.depart_reference(node)
-
- # overwritten -- we don't want source comments to show up in the HTML
- def visit_comment(self, node: Element) -> None: # type: ignore
- raise nodes.SkipNode
-
- # overwritten
- def visit_admonition(self, node: Element, name: str = '') -> None:
- self.body.append(self.starttag(
- node, 'div', CLASS=('admonition ' + name)))
- if name:
- node.insert(0, nodes.title(name, admonitionlabels[name]))
- self.set_first_last(node)
-
- def depart_admonition(self, node: Optional[Element] = None) -> None:
- self.body.append('</div>\n')
-
- def visit_seealso(self, node: Element) -> None:
- self.visit_admonition(node, 'seealso')
-
- def depart_seealso(self, node: Element) -> None:
- self.depart_admonition(node)
-
- def get_secnumber(self, node: Element) -> Optional[Tuple[int, ...]]:
- if node.get('secnumber'):
- return node['secnumber']
- elif isinstance(node.parent, nodes.section):
- if self.builder.name == 'singlehtml':
- docname = self.docnames[-1]
- anchorname = "%s/#%s" % (docname, node.parent['ids'][0])
- if anchorname not in self.builder.secnumbers:
- anchorname = "%s/" % docname # try first heading which has no anchor
- else:
- anchorname = '#' + node.parent['ids'][0]
- if anchorname not in self.builder.secnumbers:
- anchorname = '' # try first heading which has no anchor
-
- if self.builder.secnumbers.get(anchorname):
- return self.builder.secnumbers[anchorname]
-
- return None
-
- def add_secnumber(self, node: Element) -> None:
- secnumber = self.get_secnumber(node)
- if secnumber:
- self.body.append('<span class="section-number">%s</span>' %
- ('.'.join(map(str, secnumber)) + self.secnumber_suffix))
-
- def add_fignumber(self, node: Element) -> None:
- def append_fignumber(figtype: str, figure_id: str) -> None:
- if self.builder.name == 'singlehtml':
- key = "%s/%s" % (self.docnames[-1], figtype)
- else:
- key = figtype
-
- if figure_id in self.builder.fignumbers.get(key, {}):
- self.body.append('<span class="caption-number">')
- prefix = self.config.numfig_format.get(figtype)
- if prefix is None:
- msg = __('numfig_format is not defined for %s') % figtype
- logger.warning(msg)
- else:
- numbers = self.builder.fignumbers[key][figure_id]
- self.body.append(prefix % '.'.join(map(str, numbers)) + ' ')
- self.body.append('</span>')
-
- figtype = self.builder.env.domains['std'].get_enumerable_node_type(node)
- if figtype:
- if len(node['ids']) == 0:
- msg = __('Any IDs not assigned for %s node') % node.tagname
- logger.warning(msg, location=node)
- else:
- append_fignumber(figtype, node['ids'][0])
-
- def add_permalink_ref(self, node: Element, title: str) -> None:
- if node['ids'] and self.config.html_permalinks and self.builder.add_permalinks:
- format = '<a class="headerlink" href="#%s" title="%s">%s</a>'
- self.body.append(format % (node['ids'][0], title,
- self.config.html_permalinks_icon))
-
- def generate_targets_for_listing(self, node: Element) -> None:
- """Generate hyperlink targets for listings.
-
- Original visit_bullet_list(), visit_definition_list() and visit_enumerated_list()
- generates hyperlink targets inside listing tags (<ul>, <ol> and <dl>) if multiple
- IDs are assigned to listings. That is invalid DOM structure.
- (This is a bug of docutils <= 0.12)
-
- This exports hyperlink targets before listings to make valid DOM structure.
- """
- for id in node['ids'][1:]:
- self.body.append('<span id="%s"></span>' % id)
- node['ids'].remove(id)
-
- # overwritten
- def visit_bullet_list(self, node: Element) -> None:
- if len(node) == 1 and isinstance(node[0], addnodes.toctree):
- # avoid emitting empty <ul></ul>
- raise nodes.SkipNode
- self.generate_targets_for_listing(node)
- super().visit_bullet_list(node)
-
- # overwritten
- def visit_enumerated_list(self, node: Element) -> None:
- self.generate_targets_for_listing(node)
- super().visit_enumerated_list(node)
-
- # overwritten
- def visit_definition(self, node: Element) -> None:
- # don't insert </dt> here.
- self.body.append(self.starttag(node, 'dd', ''))
-
- # overwritten
- def depart_definition(self, node: Element) -> None:
- self.body.append('</dd>\n')
-
- # overwritten
- def visit_classifier(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'span', '', CLASS='classifier'))
-
- # overwritten
- def depart_classifier(self, node: Element) -> None:
- self.body.append('</span>')
-
- next_node: Node = node.next_node(descend=False, siblings=True)
- if not isinstance(next_node, nodes.classifier):
- # close `<dt>` tag at the tail of classifiers
- self.body.append('</dt>')
-
- # overwritten
- def visit_term(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'dt', ''))
-
- # overwritten
- def depart_term(self, node: Element) -> None:
- next_node: Node = node.next_node(descend=False, siblings=True)
- if isinstance(next_node, nodes.classifier):
- # Leave the end tag to `self.depart_classifier()`, in case
- # there's a classifier.
- pass
- else:
- if isinstance(node.parent.parent.parent, addnodes.glossary):
- # add permalink if glossary terms
- self.add_permalink_ref(node, _('Permalink to this term'))
-
- self.body.append('</dt>')
-
- # overwritten
- def visit_title(self, node: Element) -> None:
- if isinstance(node.parent, addnodes.compact_paragraph) and node.parent.get('toctree'):
- self.body.append(self.starttag(node, 'p', '', CLASS='caption', ROLE='heading'))
- self.body.append('<span class="caption-text">')
- self.context.append('</span></p>\n')
- else:
- super().visit_title(node)
- self.add_secnumber(node)
- self.add_fignumber(node.parent)
- if isinstance(node.parent, nodes.table):
- self.body.append('<span class="caption-text">')
-
- def depart_title(self, node: Element) -> None:
- close_tag = self.context[-1]
- if (self.config.html_permalinks and self.builder.add_permalinks and
- node.parent.hasattr('ids') and node.parent['ids']):
- # add permalink anchor
- if close_tag.startswith('</h'):
- self.add_permalink_ref(node.parent, _('Permalink to this heading'))
- elif close_tag.startswith('</a></h'):
- self.body.append('</a><a class="headerlink" href="#%s" ' %
- node.parent['ids'][0] +
- 'title="%s">%s' % (
- _('Permalink to this heading'),
- self.config.html_permalinks_icon))
- elif isinstance(node.parent, nodes.table):
- self.body.append('</span>')
- self.add_permalink_ref(node.parent, _('Permalink to this table'))
- elif isinstance(node.parent, nodes.table):
- self.body.append('</span>')
-
- super().depart_title(node)
-
- # overwritten
- def visit_literal_block(self, node: Element) -> None:
- if node.rawsource != node.astext():
- # most probably a parsed-literal block -- don't highlight
- return super().visit_literal_block(node)
-
- lang = node.get('language', 'default')
- linenos = node.get('linenos', False)
- highlight_args = node.get('highlight_args', {})
- highlight_args['force'] = node.get('force', False)
- opts = self.config.highlight_options.get(lang, {})
-
- if linenos and self.config.html_codeblock_linenos_style:
- linenos = self.config.html_codeblock_linenos_style
-
- highlighted = self.highlighter.highlight_block(
- node.rawsource, lang, opts=opts, linenos=linenos,
- location=node, **highlight_args
- )
- starttag = self.starttag(node, 'div', suffix='',
- CLASS='highlight-%s notranslate' % lang)
- self.body.append(starttag + highlighted + '</div>\n')
- raise nodes.SkipNode
-
- def visit_caption(self, node: Element) -> None:
- if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
- self.body.append('<div class="code-block-caption">')
- else:
- super().visit_caption(node)
- self.add_fignumber(node.parent)
- self.body.append(self.starttag(node, 'span', '', CLASS='caption-text'))
-
- def depart_caption(self, node: Element) -> None:
- self.body.append('</span>')
-
- # append permalink if available
- if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
- self.add_permalink_ref(node.parent, _('Permalink to this code'))
- elif isinstance(node.parent, nodes.figure):
- self.add_permalink_ref(node.parent, _('Permalink to this image'))
- elif node.parent.get('toctree'):
- self.add_permalink_ref(node.parent.parent, _('Permalink to this toctree'))
-
- if isinstance(node.parent, nodes.container) and node.parent.get('literal_block'):
- self.body.append('</div>\n')
- else:
- super().depart_caption(node)
-
- def visit_doctest_block(self, node: Element) -> None:
- self.visit_literal_block(node)
-
- # overwritten to add the <div> (for XHTML compliance)
- def visit_block_quote(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'blockquote') + '<div>')
-
- def depart_block_quote(self, node: Element) -> None:
- self.body.append('</div></blockquote>\n')
-
- # overwritten
- def visit_literal(self, node: Element) -> None:
- if 'kbd' in node['classes']:
- self.body.append(self.starttag(node, 'kbd', '',
- CLASS='docutils literal notranslate'))
- return
- lang = node.get("language", None)
- if 'code' not in node['classes'] or not lang:
- self.body.append(self.starttag(node, 'code', '',
- CLASS='docutils literal notranslate'))
- self.protect_literal_text += 1
- return
-
- opts = self.config.highlight_options.get(lang, {})
- highlighted = self.highlighter.highlight_block(
- node.astext(), lang, opts=opts, location=node, nowrap=True)
- starttag = self.starttag(
- node,
- "code",
- suffix="",
- CLASS="docutils literal highlight highlight-%s" % lang,
- )
- self.body.append(starttag + highlighted.strip() + "</code>")
- raise nodes.SkipNode
-
- def depart_literal(self, node: Element) -> None:
- if 'kbd' in node['classes']:
- self.body.append('</kbd>')
- else:
- self.protect_literal_text -= 1
- self.body.append('</code>')
-
- def visit_productionlist(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'pre'))
- names = []
- productionlist = cast(Iterable[addnodes.production], node)
- for production in productionlist:
- names.append(production['tokenname'])
- maxlen = max(len(name) for name in names)
- lastname = None
- for production in productionlist:
- if production['tokenname']:
- lastname = production['tokenname'].ljust(maxlen)
- self.body.append(self.starttag(production, 'strong', ''))
- self.body.append(lastname + '</strong> ::= ')
- elif lastname is not None:
- self.body.append('%s ' % (' ' * len(lastname)))
- production.walkabout(self)
- self.body.append('\n')
- self.body.append('</pre>\n')
- raise nodes.SkipNode
-
- def depart_productionlist(self, node: Element) -> None:
- pass
-
- def visit_production(self, node: Element) -> None:
- pass
-
- def depart_production(self, node: Element) -> None:
- pass
-
- def visit_centered(self, node: Element) -> None:
- self.body.append(self.starttag(node, 'p', CLASS="centered") +
- '<strong>')
-
- def depart_centered(self, node: Element) -> None:
- self.body.append('</strong></p>')
-
- # overwritten
- def should_be_compact_paragraph(self, node: Node) -> bool:
- """Determine if the <p> tags around paragraph can be omitted."""
- if isinstance(node.parent, addnodes.desc_content):
- # Never compact desc_content items.
- return False
- if isinstance(node.parent, addnodes.versionmodified):
- # Never compact versionmodified nodes.
- return False
- return super().should_be_compact_paragraph(node)
-
- def visit_compact_paragraph(self, node: Element) -> None:
- pass
-
- def depart_compact_paragraph(self, node: Element) -> None:
- pass
-
- def visit_download_reference(self, node: Element) -> None:
- atts = {'class': 'reference download',
- 'download': ''}
-
- if not self.builder.download_support:
- self.context.append('')
- elif 'refuri' in node:
- atts['class'] += ' external'
- atts['href'] = node['refuri']
- self.body.append(self.starttag(node, 'a', '', **atts))
- self.context.append('</a>')
- elif 'filename' in node:
- atts['class'] += ' internal'
- atts['href'] = posixpath.join(self.builder.dlpath,
- urllib.parse.quote(node['filename']))
- self.body.append(self.starttag(node, 'a', '', **atts))
- self.context.append('</a>')
- else:
- self.context.append('')
-
- def depart_download_reference(self, node: Element) -> None:
- self.body.append(self.context.pop())
-
- # overwritten
- def visit_figure(self, node: Element) -> None:
- # set align=default if align not specified to give a default style
- node.setdefault('align', 'default')
-
- return super().visit_figure(node)
-
- # overwritten
- def visit_image(self, node: Element) -> None:
- olduri = node['uri']
- # rewrite the URI if the environment knows about it
- if olduri in self.builder.images:
- node['uri'] = posixpath.join(self.builder.imgpath,
- urllib.parse.quote(self.builder.images[olduri]))
-
- if 'scale' in node:
- # Try to figure out image height and width. Docutils does that too,
- # but it tries the final file name, which does not necessarily exist
- # yet at the time the HTML file is written.
- if not ('width' in node and 'height' in node):
- size = get_image_size(os.path.join(self.builder.srcdir, olduri))
- if size is None:
- logger.warning(
- __('Could not obtain image size. :scale: option is ignored.'),
- location=node,
- )
- else:
- if 'width' not in node:
- node['width'] = str(size[0])
- if 'height' not in node:
- node['height'] = str(size[1])
-
- uri = node['uri']
- if uri.lower().endswith(('svg', 'svgz')):
- atts = {'src': uri}
- if 'width' in node:
- atts['width'] = node['width']
- if 'height' in node:
- atts['height'] = node['height']
- if 'scale' in node:
- if 'width' in atts:
- atts['width'] = multiply_length(atts['width'], node['scale'])
- if 'height' in atts:
- atts['height'] = multiply_length(atts['height'], node['scale'])
- atts['alt'] = node.get('alt', uri)
- if 'align' in node:
- atts['class'] = 'align-%s' % node['align']
- self.body.append(self.emptytag(node, 'img', '', **atts))
- return
-
- super().visit_image(node)
-
- # overwritten
- def depart_image(self, node: Element) -> None:
- if node['uri'].lower().endswith(('svg', 'svgz')):
- pass
- else:
- super().depart_image(node)
-
- def visit_toctree(self, node: Element) -> None:
- # this only happens when formatting a toc from env.tocs -- in this
- # case we don't want to include the subtree
- raise nodes.SkipNode
-
- def visit_index(self, node: Element) -> None:
- raise nodes.SkipNode
-
- def visit_tabular_col_spec(self, node: Element) -> None:
- raise nodes.SkipNode
-
- def visit_glossary(self, node: Element) -> None:
- pass
-
- def depart_glossary(self, node: Element) -> None:
- pass
-
- def visit_acks(self, node: Element) -> None:
- pass
-
- def depart_acks(self, node: Element) -> None:
- pass
-
- def visit_hlist(self, node: Element) -> None:
- self.body.append('<table class="hlist"><tr>')
-
- def depart_hlist(self, node: Element) -> None:
- self.body.append('</tr></table>\n')
-
- def visit_hlistcol(self, node: Element) -> None:
- self.body.append('<td>')
-
- def depart_hlistcol(self, node: Element) -> None:
- self.body.append('</td>')
-
- def visit_option_group(self, node: Element) -> None:
- super().visit_option_group(node)
- self.context[-2] = self.context[-2].replace('&nbsp;', '&#160;')
-
- # overwritten
- def visit_Text(self, node: Text) -> None:
- text = node.astext()
- encoded = self.encode(text)
- if self.protect_literal_text:
- # moved here from base class's visit_literal to support
- # more formatting in literal nodes
- for token in self.words_and_spaces.findall(encoded):
- if token.strip():
- # protect literal text from line wrapping
- self.body.append('<span class="pre">%s</span>' % token)
- elif token in ' \n':
- # allow breaks at whitespace
- self.body.append(token)
- else:
- # protect runs of multiple spaces; the last one can wrap
- self.body.append('&#160;' * (len(token) - 1) + ' ')
- else:
- if self.in_mailto and self.settings.cloak_email_addresses:
- encoded = self.cloak_email(encoded)
- self.body.append(encoded)
-
- def visit_note(self, node: Element) -> None:
- self.visit_admonition(node, 'note')
-
- def depart_note(self, node: Element) -> None:
- self.depart_admonition(node)
-
- def visit_warning(self, node: Element) -> None:
- self.visit_admonition(node, 'warning')
-
- def depart_warning(self, node: Element) -> None:
- self.depart_admonition(node)
-
- def visit_attention(self, node: Element) -> None:
- self.visit_admonition(node, 'attention')
-
- def depart_attention(self, node: Element) -> None:
- self.depart_admonition(node)
-
- def visit_caution(self, node: Element) -> None:
- self.visit_admonition(node, 'caution')
-
- def depart_caution(self, node: Element) -> None:
- self.depart_admonition(node)
-
- def visit_danger(self, node: Element) -> None:
- self.visit_admonition(node, 'danger')
-
- def depart_danger(self, node: Element) -> None:
- self.depart_admonition(node)
-
- def visit_error(self, node: Element) -> None:
- self.visit_admonition(node, 'error')
-
- def depart_error(self, node: Element) -> None:
- self.depart_admonition(node)
-
- def visit_hint(self, node: Element) -> None:
- self.visit_admonition(node, 'hint')
-
- def depart_hint(self, node: Element) -> None:
- self.depart_admonition(node)
-
- def visit_important(self, node: Element) -> None:
- self.visit_admonition(node, 'important')
-
- def depart_important(self, node: Element) -> None:
- self.depart_admonition(node)
-
- def visit_tip(self, node: Element) -> None:
- self.visit_admonition(node, 'tip')
-
- def depart_tip(self, node: Element) -> None:
- self.depart_admonition(node)
-
- def visit_literal_emphasis(self, node: Element) -> None:
- return self.visit_emphasis(node)
-
- def depart_literal_emphasis(self, node: Element) -> None:
- return self.depart_emphasis(node)
-
- def visit_literal_strong(self, node: Element) -> None:
- return self.visit_strong(node)
-
- def depart_literal_strong(self, node: Element) -> None:
- return self.depart_strong(node)
-
- def visit_abbreviation(self, node: Element) -> None:
- attrs = {}
- if node.hasattr('explanation'):
- attrs['title'] = node['explanation']
- self.body.append(self.starttag(node, 'abbr', '', **attrs))
-
- def depart_abbreviation(self, node: Element) -> None:
- self.body.append('</abbr>')
-
- def visit_manpage(self, node: Element) -> None:
- self.visit_literal_emphasis(node)
- if self.manpages_url:
- node['refuri'] = self.manpages_url.format(**node.attributes)
- self.visit_reference(node)
-
- def depart_manpage(self, node: Element) -> None:
- if self.manpages_url:
- self.depart_reference(node)
- self.depart_literal_emphasis(node)
-
- # overwritten to add even/odd classes
-
- def visit_table(self, node: Element) -> None:
- self._table_row_indices.append(0)
-
- # set align=default if align not specified to give a default style
- node.setdefault('align', 'default')
-
- return super().visit_table(node)
-
- def depart_table(self, node: Element) -> None:
- self._table_row_indices.pop()
- super().depart_table(node)
-
- def visit_row(self, node: Element) -> None:
- self._table_row_indices[-1] += 1
- if self._table_row_indices[-1] % 2 == 0:
- node['classes'].append('row-even')
- else:
- node['classes'].append('row-odd')
- self.body.append(self.starttag(node, 'tr', ''))
- node.column = 0 # type: ignore
-
- def visit_entry(self, node: Element) -> None:
- super().visit_entry(node)
- if self.body[-1] == '&nbsp;':
- self.body[-1] = '&#160;'
-
- def visit_field_list(self, node: Element) -> None:
- self._fieldlist_row_indices.append(0)
- return super().visit_field_list(node)
-
- def depart_field_list(self, node: Element) -> None:
- self._fieldlist_row_indices.pop()
- return super().depart_field_list(node)
-
- def visit_field(self, node: Element) -> None:
- self._fieldlist_row_indices[-1] += 1
- if self._fieldlist_row_indices[-1] % 2 == 0:
- node['classes'].append('field-even')
- else:
- node['classes'].append('field-odd')
- self.body.append(self.starttag(node, 'tr', '', CLASS='field'))
-
- def visit_field_name(self, node: Element) -> None:
- context_count = len(self.context)
- super().visit_field_name(node)
- if context_count != len(self.context):
- self.context[-1] = self.context[-1].replace('&nbsp;', '&#160;')
-
- def visit_math(self, node: Element, math_env: str = '') -> None:
- name = self.builder.math_renderer_name
- visit, _ = self.builder.app.registry.html_inline_math_renderers[name]
- visit(self, node)
-
- def depart_math(self, node: Element, math_env: str = '') -> None:
- name = self.builder.math_renderer_name
- _, depart = self.builder.app.registry.html_inline_math_renderers[name]
- if depart: # type: ignore[truthy-function]
- depart(self, node)
-
- def visit_math_block(self, node: Element, math_env: str = '') -> None:
- name = self.builder.math_renderer_name
- visit, _ = self.builder.app.registry.html_block_math_renderers[name]
- visit(self, node)
-
- def depart_math_block(self, node: Element, math_env: str = '') -> None:
- name = self.builder.math_renderer_name
- _, depart = self.builder.app.registry.html_block_math_renderers[name]
- if depart: # type: ignore[truthy-function]
- depart(self, node)