Remove HTML 4 support (#11385)

This commit is contained in:
Adam Turner
2023-04-28 11:32:12 +01:00
committed by GitHub
parent 3e3251d3ba
commit ad473730a3
5 changed files with 25 additions and 899 deletions

View File

@@ -45,7 +45,6 @@ from sphinx.util.inventory import InventoryFile
from sphinx.util.matching import DOTFILES, Matcher, patmatch
from sphinx.util.osutil import copyfile, ensuredir, os_path, relative_uri
from sphinx.util.tags import Tags
from sphinx.writers._html4 import HTML4Translator
from sphinx.writers.html import HTMLWriter
from sphinx.writers.html5 import HTML5Translator
@@ -374,10 +373,7 @@ class StandaloneHTMLBuilder(Builder):
@property
def default_translator_class(self) -> type[nodes.NodeVisitor]: # type: ignore
if self.config.html4_writer:
return HTML4Translator # RemovedInSphinx70Warning
else:
return HTML5Translator
return HTML5Translator
@property
def math_renderer_name(self) -> str:
@@ -565,7 +561,7 @@ class StandaloneHTMLBuilder(Builder):
'parents': [],
'logo_url': logo,
'favicon_url': favicon,
'html5_doctype': not self.config.html4_writer,
'html5_doctype': True,
}
if self.theme:
self.globalcontext.update(
@@ -1310,13 +1306,13 @@ def validate_html_favicon(app: Sphinx, config: Config) -> None:
config.html_favicon = None # type: ignore
def deprecate_html_4(_app: Sphinx, config: Config) -> None:
"""Warn on HTML 4."""
# RemovedInSphinx70Warning
def error_on_html_4(_app: Sphinx, config: Config) -> None:
"""Error on HTML 4."""
if config.html4_writer:
logger.warning(_('Support for emitting HTML 4 output is deprecated and '
'will be removed in Sphinx 7. ("html4_writer=True" '
'detected in configuration options)'))
raise ConfigError(_(
'HTML 4 is no longer supported by Sphinx. '
'("html4_writer=True" detected in configuration options)',
))
def setup(app: Sphinx) -> dict[str, Any]:
@@ -1380,7 +1376,7 @@ def setup(app: Sphinx) -> dict[str, Any]:
app.connect('config-inited', validate_html_static_path, priority=800)
app.connect('config-inited', validate_html_logo, priority=800)
app.connect('config-inited', validate_html_favicon, priority=800)
app.connect('config-inited', deprecate_html_4, priority=800)
app.connect('config-inited', error_on_html_4, priority=800)
app.connect('builder-inited', validate_math_renderer)
app.connect('html-page-context', setup_css_tag_helper)
app.connect('html-page-context', setup_js_tag_helper)

View File

@@ -7,12 +7,9 @@
:copyright: Copyright 2007-2023 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
#}
{%- block doctype -%}{%- if html5_doctype %}
{%- block doctype -%}
<!DOCTYPE html>
{%- else %}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
{%- endif %}{%- endblock %}
{%- endblock %}
{%- set reldelim1 = reldelim1 is not defined and ' &#187;' or reldelim1 %}
{%- set reldelim2 = reldelim2 is not defined and ' |' or reldelim2 %}
{%- set render_sidebar = (not embedded) and (not theme_nosidebar|tobool) and
@@ -105,17 +102,10 @@
{%- if html_tag %}
{{ html_tag }}
{%- else %}
<html{% if not html5_doctype %} xmlns="http://www.w3.org/1999/xhtml"{% endif %}{% if language is not none %} lang="{{ language }}"{% endif %}>
<html{% if language is not none %} lang="{{ language }}"{% endif %}>
{%- endif %}
<head>
{%- if not html5_doctype and not skip_ua_compatible %}
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
{%- endif %}
{%- if use_meta_charset or html5_doctype %}
<meta charset="{{ encoding }}" />
{%- else %}
<meta http-equiv="Content-Type" content="text/html; charset={{ encoding }}" />
{%- endif %}
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
{{- metatags }}
{%- block htmltitle %}

View File

@@ -1,857 +0,0 @@
"""Frozen HTML 4 translator."""
from __future__ import annotations
import os
import posixpath
import re
import urllib.parse
from typing import TYPE_CHECKING, Iterable, 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
if scale == 100:
return length
amount, unit = matched.groups()
result = float(amount) * scale / 100
return f"{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) # NoQA: PT018
atts['class'] += ' image-reference'
if 'reftitle' in node:
atts['title'] = node['reftitle']
if 'target' in node:
atts['target'] = node['target']
self.body.append(self.starttag(node, 'a', '', **atts))
if node.get('secnumber'):
self.body.append(('%s' + self.secnumber_suffix) %
'.'.join(map(str, node['secnumber'])))
def visit_number_reference(self, node: Element) -> None:
self.visit_reference(node)
def depart_number_reference(self, node: Element) -> None:
self.depart_reference(node)
# overwritten -- we don't want source comments to show up in the HTML
def visit_comment(self, node: Element) -> None: # type: ignore
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: 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']
elif isinstance(node.parent, nodes.section):
if self.builder.name == 'singlehtml':
docname = self.docnames[-1]
anchorname = f"{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:
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="{}">{}'.format(
_('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

@@ -7,15 +7,14 @@ from typing import TYPE_CHECKING, cast
from docutils.writers.html4css1 import Writer
from sphinx.util import logging
from sphinx.writers._html4 import HTML4Translator
from sphinx.writers.html5 import HTML5Translator # NoQA: F401
from sphinx.writers.html5 import HTML5Translator
if TYPE_CHECKING:
from sphinx.builders.html import StandaloneHTMLBuilder
logger = logging.getLogger(__name__)
HTMLTranslator = HTML4Translator
HTMLTranslator = HTML5Translator
# A good overview of the purpose behind these classes can be found here:
# http://www.arnebrodowski.de/blog/write-your-own-restructuredtext-writer.html
@@ -33,7 +32,7 @@ class HTMLWriter(Writer):
def translate(self) -> None:
# sadly, this is mostly copied from parent class
visitor = self.builder.create_translator(self.document, self.builder)
self.visitor = cast(HTML4Translator, visitor)
self.visitor = cast(HTML5Translator, visitor)
self.document.walkabout(visitor)
self.output = self.visitor.astext()
for attr in ('head_prefix', 'stylesheet', 'head', 'body_prefix',

View File

@@ -123,19 +123,17 @@ def test_html_warnings(app, warning):
'--- Got:\n' + html_warnings
@pytest.mark.sphinx('html', confoverrides={'html4_writer': True})
def test_html4_output(app, status, warning):
app.build()
def test_html4_deprecation(make_app, tempdir):
def test_html4_error(make_app, tempdir):
(tempdir / 'conf.py').write_text('', encoding='utf-8')
app = make_app(
buildername='html',
srcdir=tempdir,
confoverrides={'html4_writer': True},
)
assert 'HTML 4 output is deprecated and will be removed' in app._warning.getvalue()
with pytest.raises(
ConfigError,
match=r'HTML 4 is no longer supported by Sphinx',
):
make_app(
buildername='html',
srcdir=tempdir,
confoverrides={'html4_writer': True},
)
@pytest.mark.parametrize("fname,expect", flat_dict({