mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
This commit adds a `level` option to the `rubric` directive, which propagates a `level` attribute to the `rubric` node, and allows renderers to select a specific heading level. Logic for this attribute has been added to the HTML5 and LaTeX builder.
948 lines
37 KiB
Python
948 lines
37 KiB
Python
"""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 <inline classes="s">...</inline> will be
|
|
# converted to <s>...</s> by `visit_inline`.
|
|
supported_inline_tags: set[str] = set()
|
|
|
|
def __init__(self, document: nodes.document, builder: Builder) -> None:
|
|
super().__init__(document, builder)
|
|
|
|
self.highlighter = self.builder.highlighter
|
|
self.docnames = [self.builder.current_docname] # for singlehtml builder
|
|
self.protect_literal_text = 0
|
|
self.secnumber_suffix = self.config.html_secnumber_suffix
|
|
self.param_separator = ''
|
|
self.optional_param_level = 0
|
|
self._table_row_indices = [0]
|
|
self._fieldlist_row_indices = [0]
|
|
self.required_params_left = 0
|
|
|
|
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, _('Link 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, _('Link 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, 'span', ''))
|
|
|
|
def depart_desc_name(self, node: Element) -> None:
|
|
self.body.append('</span>')
|
|
|
|
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('</span>')
|
|
|
|
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">→</span>')
|
|
self.body.append(' <span class="sig-return-typehint">')
|
|
|
|
def depart_desc_returns(self, node: Element) -> None:
|
|
self.body.append('</span></span>')
|
|
|
|
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'<span class="sig-paren">{sig_open_paren}</span>')
|
|
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('</dl>\n\n')
|
|
sig_close_paren = self.context.pop()
|
|
self.body.append(f'<span class="sig-paren">{sig_close_paren}</span>')
|
|
|
|
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('<em class="sig-param">')
|
|
|
|
def depart_desc_parameter(self, node: Element) -> None:
|
|
if not node.hasattr('noemph'):
|
|
self.body.append('</em>')
|
|
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('</dd>\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('<span class="optional">[</span>')
|
|
# 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('<span class="optional">[</span>')
|
|
self.body.append('</dd>\n')
|
|
# Else, open a new bracket, append the parameter separator,
|
|
# and end the line.
|
|
else:
|
|
self.body.append('<span class="optional">[</span>')
|
|
self.body.append(self.param_separator)
|
|
self.body.append('</dd>\n')
|
|
else:
|
|
self.body.append('<span class="optional">[</span>')
|
|
|
|
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('<span class="optional">]</span>')
|
|
# 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('</dd>\n')
|
|
else:
|
|
self.body.append('<span class="optional">]</span>')
|
|
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('</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) # NoQA: PT018
|
|
atts['class'] += ' image-reference'
|
|
if 'reftitle' in node:
|
|
atts['title'] = node['reftitle']
|
|
if 'target' in node:
|
|
atts['target'] = node['target']
|
|
if 'rel' in node:
|
|
atts['rel'] = node['rel']
|
|
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('</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) -> 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('<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 = f"{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:
|
|
icon = self.config.html_permalinks_icon
|
|
if node['ids'] and self.config.html_permalinks and self.builder.add_permalinks:
|
|
self.body.append(
|
|
f'<a class="headerlink" href="#{node["ids"][0]}" title="{title}">{icon}</a>',
|
|
)
|
|
|
|
# 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
|
|
super().visit_bullet_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, _('Link 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">')
|
|
# 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('<a ')
|
|
# TODO: only remove for EPUB
|
|
):
|
|
# remove <a class="reference internal" href="#top">
|
|
self.body.pop()
|
|
self.context[-1] = '</p>\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('</h'):
|
|
self.add_permalink_ref(node.parent, _('Link to this heading'))
|
|
elif close_tag.startswith('</a></h'):
|
|
self.body.append('</a><a class="headerlink" href="#%s" ' %
|
|
node.parent['ids'][0] +
|
|
'title="{}">{}'.format(
|
|
_('Link to this heading'),
|
|
self.config.html_permalinks_icon))
|
|
elif isinstance(node.parent, nodes.table):
|
|
self.body.append('</span>')
|
|
self.add_permalink_ref(node.parent, _('Link to this table'))
|
|
elif isinstance(node.parent, nodes.table):
|
|
self.body.append('</span>')
|
|
|
|
super().depart_title(node)
|
|
|
|
# overwritten
|
|
def visit_rubric(self, node: Element) -> None:
|
|
if "level" in node:
|
|
level = node["level"]
|
|
if level in (1, 2, 3, 4, 5, 6):
|
|
self.body.append(self.starttag(node, f'h{level}', '', CLASS='rubric'))
|
|
else:
|
|
logger.warning(
|
|
__('unsupported rubric heading level: %s'),
|
|
level,
|
|
type='html',
|
|
location=node
|
|
)
|
|
super().visit_rubric(node)
|
|
else:
|
|
super().visit_rubric(node)
|
|
|
|
# overwritten
|
|
def depart_rubric(self, node: Element) -> None:
|
|
if level := node.get("level"):
|
|
self.body.append(f'</h{level}>\n')
|
|
else:
|
|
super().depart_rubric(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, _('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('</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'))
|
|
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 + '</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>')
|
|
|
|
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):
|
|
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])
|
|
|
|
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>')
|
|
|
|
# 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(' ' * (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)
|
|
|
|
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('<span class="fn-bracket">[</span>')
|