Factor out HTML 4 translator (#11051)

Move the HTML 4 translator into a private module.
This commit is contained in:
Adam Turner 2022-12-30 00:53:04 +00:00 committed by GitHub
parent aa2fa38fef
commit bf06d7ef4d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 907 additions and 887 deletions

View File

@ -43,7 +43,8 @@ from sphinx.util.inventory import InventoryFile
from sphinx.util.matching import DOTFILES, Matcher, patmatch from sphinx.util.matching import DOTFILES, Matcher, patmatch
from sphinx.util.osutil import copyfile, ensuredir, os_path, relative_uri from sphinx.util.osutil import copyfile, ensuredir, os_path, relative_uri
from sphinx.util.tags import Tags from sphinx.util.tags import Tags
from sphinx.writers.html import HTMLTranslator, HTMLWriter from sphinx.writers._html4 import HTML4Translator
from sphinx.writers.html import HTMLWriter
from sphinx.writers.html5 import HTML5Translator from sphinx.writers.html5 import HTML5Translator
#: the filename for the inventory of objects #: the filename for the inventory of objects
@ -372,7 +373,7 @@ class StandaloneHTMLBuilder(Builder):
@property @property
def default_translator_class(self) -> Type[nodes.NodeVisitor]: # type: ignore def default_translator_class(self) -> Type[nodes.NodeVisitor]: # type: ignore
if self.config.html4_writer: if self.config.html4_writer:
return HTMLTranslator # RemovedInSphinx70Warning return HTML4Translator # RemovedInSphinx70Warning
else: else:
return HTML5Translator return HTML5Translator
@ -1326,8 +1327,12 @@ import sphinx.builders.singlehtml # noqa: E402,F401
deprecated_alias('sphinx.builders.html', deprecated_alias('sphinx.builders.html',
{ {
'html5_ready': True, 'html5_ready': True,
'HTMLTranslator': HTML4Translator,
}, },
RemovedInSphinx70Warning) RemovedInSphinx70Warning,
{
'HTMLTranslator': 'sphinx.writers.html.HTML5Translator',
})
def setup(app: Sphinx) -> Dict[str, Any]: def setup(app: Sphinx) -> Dict[str, Any]:

View File

@ -84,7 +84,7 @@ from sphinx.util.docutils import (NullReporter, SphinxDirective, SphinxRole, new
from sphinx.util.inspect import signature_from_str from sphinx.util.inspect import signature_from_str
from sphinx.util.matching import Matcher from sphinx.util.matching import Matcher
from sphinx.util.typing import OptionSpec from sphinx.util.typing import OptionSpec
from sphinx.writers.html import HTMLTranslator from sphinx.writers.html import HTML5Translator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -116,7 +116,7 @@ class autosummary_table(nodes.comment):
pass pass
def autosummary_table_visit_html(self: HTMLTranslator, node: autosummary_table) -> None: def autosummary_table_visit_html(self: HTML5Translator, node: autosummary_table) -> None:
"""Make the first column of the table non-breaking.""" """Make the first column of the table non-breaking."""
try: try:
table = cast(nodes.table, node[0]) table = cast(nodes.table, node[0])

View File

@ -23,7 +23,7 @@ from sphinx.util.i18n import search_image_for_language
from sphinx.util.nodes import set_source_info from sphinx.util.nodes import set_source_info
from sphinx.util.osutil import ensuredir from sphinx.util.osutil import ensuredir
from sphinx.util.typing import OptionSpec from sphinx.util.typing import OptionSpec
from sphinx.writers.html import HTMLTranslator from sphinx.writers.html import HTML5Translator
from sphinx.writers.latex import LaTeXTranslator from sphinx.writers.latex import LaTeXTranslator
from sphinx.writers.manpage import ManualPageTranslator from sphinx.writers.manpage import ManualPageTranslator
from sphinx.writers.texinfo import TexinfoTranslator from sphinx.writers.texinfo import TexinfoTranslator
@ -262,7 +262,7 @@ def render_dot(self: SphinxTranslator, code: str, options: Dict, format: str,
'[stdout]\n%r') % (exc.stderr, exc.stdout)) from exc '[stdout]\n%r') % (exc.stderr, exc.stdout)) from exc
def render_dot_html(self: HTMLTranslator, node: graphviz, code: str, options: Dict, def render_dot_html(self: HTML5Translator, node: graphviz, code: str, options: Dict,
prefix: str = 'graphviz', imgcls: Optional[str] = None, prefix: str = 'graphviz', imgcls: Optional[str] = None,
alt: Optional[str] = None, filename: Optional[str] = None alt: Optional[str] = None, filename: Optional[str] = None
) -> Tuple[str, str]: ) -> Tuple[str, str]:
@ -315,7 +315,7 @@ def render_dot_html(self: HTMLTranslator, node: graphviz, code: str, options: Di
raise nodes.SkipNode raise nodes.SkipNode
def html_visit_graphviz(self: HTMLTranslator, node: graphviz) -> None: def html_visit_graphviz(self: HTML5Translator, node: graphviz) -> None:
render_dot_html(self, node, node['code'], node['options'], filename=node.get('filename')) render_dot_html(self, node, node['code'], node['options'], filename=node.get('filename'))

View File

@ -24,7 +24,7 @@ from sphinx.util.math import get_node_equation_number, wrap_displaymath
from sphinx.util.osutil import ensuredir from sphinx.util.osutil import ensuredir
from sphinx.util.png import read_png_depth, write_png_depth from sphinx.util.png import read_png_depth, write_png_depth
from sphinx.util.template import LaTeXRenderer from sphinx.util.template import LaTeXRenderer
from sphinx.writers.html import HTMLTranslator from sphinx.writers.html import HTML5Translator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -201,7 +201,7 @@ def convert_dvi_to_svg(dvipath: str, builder: Builder, out_path: str) -> Optiona
def render_math( def render_math(
self: HTMLTranslator, self: HTML5Translator,
math: str, math: str,
) -> Tuple[Optional[str], Optional[int]]: ) -> Tuple[Optional[str], Optional[int]]:
"""Render the LaTeX math expression *math* using latex and dvipng or """Render the LaTeX math expression *math* using latex and dvipng or
@ -291,13 +291,13 @@ def clean_up_files(app: Sphinx, exc: Exception) -> None:
pass pass
def get_tooltip(self: HTMLTranslator, node: Element) -> str: def get_tooltip(self: HTML5Translator, node: Element) -> str:
if self.builder.config.imgmath_add_tooltips: if self.builder.config.imgmath_add_tooltips:
return ' alt="%s"' % self.encode(node.astext()).strip() return ' alt="%s"' % self.encode(node.astext()).strip()
return '' return ''
def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None: def html_visit_math(self: HTML5Translator, node: nodes.math) -> None:
try: try:
rendered_path, depth = render_math(self, '$' + node.astext() + '$') rendered_path, depth = render_math(self, '$' + node.astext() + '$')
except MathExtError as exc: except MathExtError as exc:
@ -326,7 +326,7 @@ def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None:
raise nodes.SkipNode raise nodes.SkipNode
def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None: def html_visit_displaymath(self: HTML5Translator, node: nodes.math_block) -> None:
if node['nowrap']: if node['nowrap']:
latex = node.astext() latex = node.astext()
else: else:

View File

@ -47,7 +47,7 @@ from sphinx.ext.graphviz import (figure_wrapper, graphviz, render_dot_html, rend
from sphinx.util import md5 from sphinx.util import md5
from sphinx.util.docutils import SphinxDirective from sphinx.util.docutils import SphinxDirective
from sphinx.util.typing import OptionSpec from sphinx.util.typing import OptionSpec
from sphinx.writers.html import HTMLTranslator from sphinx.writers.html import HTML5Translator
from sphinx.writers.latex import LaTeXTranslator from sphinx.writers.latex import LaTeXTranslator
from sphinx.writers.texinfo import TexinfoTranslator from sphinx.writers.texinfo import TexinfoTranslator
@ -387,7 +387,7 @@ def get_graph_hash(node: inheritance_diagram) -> str:
return md5(encoded).hexdigest()[-10:] return md5(encoded).hexdigest()[-10:]
def html_visit_inheritance_diagram(self: HTMLTranslator, node: inheritance_diagram) -> None: def html_visit_inheritance_diagram(self: HTML5Translator, node: inheritance_diagram) -> None:
""" """
Output the graph for HTML. This will insert a PNG with clickable Output the graph for HTML. This will insert a PNG with clickable
image map. image map.

View File

@ -16,7 +16,7 @@ from sphinx.domains.math import MathDomain
from sphinx.errors import ExtensionError from sphinx.errors import ExtensionError
from sphinx.locale import _ from sphinx.locale import _
from sphinx.util.math import get_node_equation_number from sphinx.util.math import get_node_equation_number
from sphinx.writers.html import HTMLTranslator from sphinx.writers.html import HTML5Translator
# more information for mathjax secure url is here: # more information for mathjax secure url is here:
# https://docs.mathjax.org/en/latest/start.html#secure-access-to-the-cdn # https://docs.mathjax.org/en/latest/start.html#secure-access-to-the-cdn
@ -25,7 +25,7 @@ MATHJAX_URL = 'https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'
logger = sphinx.util.logging.getLogger(__name__) logger = sphinx.util.logging.getLogger(__name__)
def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None: def html_visit_math(self: HTML5Translator, node: nodes.math) -> None:
self.body.append(self.starttag(node, 'span', '', CLASS='math notranslate nohighlight')) self.body.append(self.starttag(node, 'span', '', CLASS='math notranslate nohighlight'))
self.body.append(self.builder.config.mathjax_inline[0] + self.body.append(self.builder.config.mathjax_inline[0] +
self.encode(node.astext()) + self.encode(node.astext()) +
@ -33,7 +33,7 @@ def html_visit_math(self: HTMLTranslator, node: nodes.math) -> None:
raise nodes.SkipNode raise nodes.SkipNode
def html_visit_displaymath(self: HTMLTranslator, node: nodes.math_block) -> None: def html_visit_displaymath(self: HTML5Translator, node: nodes.math_block) -> None:
self.body.append(self.starttag(node, 'div', CLASS='math notranslate nohighlight')) self.body.append(self.starttag(node, 'div', CLASS='math notranslate nohighlight'))
if node['nowrap']: if node['nowrap']:
self.body.append(self.encode(node.astext())) self.body.append(self.encode(node.astext()))

View File

@ -22,7 +22,7 @@ from sphinx.locale import _, __
from sphinx.util import logging, texescape from sphinx.util import logging, texescape
from sphinx.util.docutils import SphinxDirective, new_document from sphinx.util.docutils import SphinxDirective, new_document
from sphinx.util.typing import OptionSpec from sphinx.util.typing import OptionSpec
from sphinx.writers.html import HTMLTranslator from sphinx.writers.html import HTML5Translator
from sphinx.writers.latex import LaTeXTranslator from sphinx.writers.latex import LaTeXTranslator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -188,14 +188,14 @@ class TodoListProcessor:
self.document.remove(todo) self.document.remove(todo)
def visit_todo_node(self: HTMLTranslator, node: todo_node) -> None: def visit_todo_node(self: HTML5Translator, node: todo_node) -> None:
if self.config.todo_include_todos: if self.config.todo_include_todos:
self.visit_admonition(node) self.visit_admonition(node)
else: else:
raise nodes.SkipNode raise nodes.SkipNode
def depart_todo_node(self: HTMLTranslator, node: todo_node) -> None: def depart_todo_node(self: HTML5Translator, node: todo_node) -> None:
self.depart_admonition(node) self.depart_admonition(node)

View File

@ -4,10 +4,10 @@ from typing import Optional
from docutils import nodes from docutils import nodes
from sphinx.builders.html import HTMLTranslator from sphinx.builders.html import HTML5Translator
def get_node_equation_number(writer: HTMLTranslator, node: nodes.math_block) -> str: def get_node_equation_number(writer: HTML5Translator, node: nodes.math_block) -> str:
if writer.builder.config.math_numfig and writer.builder.config.numfig: if writer.builder.config.math_numfig and writer.builder.config.numfig:
figtype = 'displaymath' figtype = 'displaymath'
if writer.builder.name == 'singlehtml': if writer.builder.name == 'singlehtml':

856
sphinx/writers/_html4.py Normal file
View File

@ -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)

View File

@ -1,46 +1,24 @@
"""docutils writers handling Sphinx' custom nodes.""" """docutils writers handling Sphinx' custom nodes."""
import os from typing import TYPE_CHECKING, cast
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 docutils.writers.html4css1 import Writer 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 import logging
from sphinx.util.docutils import SphinxTranslator from sphinx.writers._html4 import HTML4Translator
from sphinx.util.images import get_image_size from sphinx.writers.html5 import HTML5Translator # NoQA: F401
if TYPE_CHECKING: if TYPE_CHECKING:
from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.builders.html import StandaloneHTMLBuilder
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
HTMLTranslator = HTML4Translator
# A good overview of the purpose behind these classes can be found here: # A good overview of the purpose behind these classes can be found here:
# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html # 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): class HTMLWriter(Writer):
# override embed-stylesheet default value to False. # override embed-stylesheet default value to False.
@ -53,7 +31,7 @@ class HTMLWriter(Writer):
def translate(self) -> None: def translate(self) -> None:
# sadly, this is mostly copied from parent class # sadly, this is mostly copied from parent class
visitor = self.builder.create_translator(self.document, self.builder) visitor = self.builder.create_translator(self.document, self.builder)
self.visitor = cast(HTMLTranslator, visitor) self.visitor = cast(HTML4Translator, visitor)
self.document.walkabout(visitor) self.document.walkabout(visitor)
self.output = self.visitor.astext() self.output = self.visitor.astext()
for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix', for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix',
@ -63,822 +41,3 @@ class HTMLWriter(Writer):
'html_subtitle', 'html_body', ): 'html_subtitle', 'html_body', ):
setattr(self, attr, getattr(visitor, attr, None)) setattr(self, attr, getattr(visitor, attr, None))
self.clean_meta = ''.join(self.visitor.meta[2:]) 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)

View File

@ -5,7 +5,7 @@
from docutils.writers.docutils_xml import XMLTranslator from docutils.writers.docutils_xml import XMLTranslator
from sphinx.writers.html import HTMLTranslator from sphinx.writers.html import HTML5Translator
from sphinx.writers.latex import LaTeXTranslator from sphinx.writers.latex import LaTeXTranslator
from sphinx.writers.manpage import ManualPageTranslator from sphinx.writers.manpage import ManualPageTranslator
from sphinx.writers.texinfo import TexinfoTranslator from sphinx.writers.texinfo import TexinfoTranslator
@ -14,23 +14,23 @@ from sphinx.writers.text import TextTranslator
project = 'test' project = 'test'
class ConfHTMLTranslator(HTMLTranslator): class ConfHTMLTranslator(HTML5Translator):
pass pass
class ConfDirHTMLTranslator(HTMLTranslator): class ConfDirHTMLTranslator(HTML5Translator):
pass pass
class ConfSingleHTMLTranslator(HTMLTranslator): class ConfSingleHTMLTranslator(HTML5Translator):
pass pass
class ConfPickleTranslator(HTMLTranslator): class ConfPickleTranslator(HTML5Translator):
pass pass
class ConfJsonTranslator(HTMLTranslator): class ConfJsonTranslator(HTML5Translator):
pass pass

View File

@ -1,5 +1,5 @@
from sphinx.writers.html import HTMLTranslator from sphinx.writers.html import HTML5Translator
class ExtHTMLTranslator(HTMLTranslator): class ExtHTMLTranslator(HTML5Translator):
pass pass

View File

@ -1,15 +1,15 @@
from sphinx.writers.html import HTMLTranslator from sphinx.writers.html import HTML5Translator
project = 'test' project = 'test'
class ConfHTMLTranslator(HTMLTranslator): class ConfHTMLTranslator(HTML5Translator):
depart_with_node = 0 depart_with_node = 0
def depart_admonition(self, node=None): def depart_admonition(self, node=None):
if node is not None: if node is not None:
self.depart_with_node += 1 self.depart_with_node += 1
HTMLTranslator.depart_admonition(self, node) HTML5Translator.depart_admonition(self, node)
def setup(app): def setup(app):

View File

@ -16,7 +16,7 @@ from sphinx.testing.util import Struct, assert_node
from sphinx.transforms import SphinxSmartQuotes from sphinx.transforms import SphinxSmartQuotes
from sphinx.util import texescape from sphinx.util import texescape
from sphinx.util.docutils import sphinx_domains from sphinx.util.docutils import sphinx_domains
from sphinx.writers.html import HTMLTranslator, HTMLWriter from sphinx.writers.html import HTML5Translator, HTMLWriter
from sphinx.writers.latex import LaTeXTranslator, LaTeXWriter from sphinx.writers.latex import LaTeXTranslator, LaTeXWriter
@ -81,7 +81,7 @@ class ForgivingTranslator:
pass pass
class ForgivingHTMLTranslator(HTMLTranslator, ForgivingTranslator): class ForgivingHTMLTranslator(HTML5Translator, ForgivingTranslator):
pass pass
@ -357,27 +357,27 @@ def get_verifier(verify, verify_re):
# description list: simple # description list: simple
'verify', 'verify',
'term\n description', 'term\n description',
'<dl class="docutils">\n<dt>term</dt><dd>description</dd>\n</dl>', '<dl class="simple">\n<dt>term</dt><dd><p>description</p>\n</dd>\n</dl>',
None, None,
), ),
( (
# description list: with classifiers # description list: with classifiers
'verify', 'verify',
'term : class1 : class2\n description', 'term : class1 : class2\n description',
('<dl class="docutils">\n<dt>term<span class="classifier">class1</span>' ('<dl class="simple">\n<dt>term<span class="classifier">class1</span>'
'<span class="classifier">class2</span></dt><dd>description</dd>\n</dl>'), '<span class="classifier">class2</span></dt><dd><p>description</p>\n</dd>\n</dl>'),
None, None,
), ),
( (
# glossary (description list): multiple terms # glossary (description list): multiple terms
'verify', 'verify',
'.. glossary::\n\n term1\n term2\n description', '.. glossary::\n\n term1\n term2\n description',
('<dl class="glossary docutils">\n' ('<dl class="simple glossary">\n'
'<dt id="term-term1">term1<a class="headerlink" href="#term-term1"' '<dt id="term-term1">term1<a class="headerlink" href="#term-term1"'
' title="Permalink to this term">¶</a></dt>' ' title="Permalink to this term">¶</a></dt>'
'<dt id="term-term2">term2<a class="headerlink" href="#term-term2"' '<dt id="term-term2">term2<a class="headerlink" href="#term-term2"'
' title="Permalink to this term">¶</a></dt>' ' title="Permalink to this term">¶</a></dt>'
'<dd>description</dd>\n</dl>'), '<dd><p>description</p>\n</dd>\n</dl>'),
None, None,
), ),
]) ])