From ad473730a3763f241e9aaea1a87d1893f01b86fd Mon Sep 17 00:00:00 2001 From: Adam Turner <9087854+AA-Turner@users.noreply.github.com> Date: Fri, 28 Apr 2023 11:32:12 +0100 Subject: [PATCH] Remove HTML 4 support (#11385) --- sphinx/builders/html/__init__.py | 22 +- sphinx/themes/basic/layout.html | 16 +- sphinx/writers/_html4.py | 857 ------------------------------- sphinx/writers/html.py | 7 +- tests/test_build_html.py | 22 +- 5 files changed, 25 insertions(+), 899 deletions(-) delete mode 100644 sphinx/writers/_html4.py diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index cd6e9498ee5..8b8c426a4b0 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -45,7 +45,6 @@ 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 @@ def add_js_file(self, filename: str, **kwargs: Any) -> None: @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 @@ def prepare_writing(self, docnames: set[str]) -> None: '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) diff --git a/sphinx/themes/basic/layout.html b/sphinx/themes/basic/layout.html index 6e9096a1dbd..f3088f79a95 100644 --- a/sphinx/themes/basic/layout.html +++ b/sphinx/themes/basic/layout.html @@ -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 -%} -{%- else %} - -{%- endif %}{%- endblock %} +{%- endblock %} {%- set reldelim1 = reldelim1 is not defined and ' »' 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 @@

{{ _('Navigation') }}

{%- if html_tag %} {{ html_tag }} {%- else %} - + {%- endif %} - {%- if not html5_doctype and not skip_ua_compatible %} - - {%- endif %} - {%- if use_meta_charset or html5_doctype %} - {%- else %} - - {%- endif %} {{- metatags }} {%- block htmltitle %} diff --git a/sphinx/writers/_html4.py b/sphinx/writers/_html4.py deleted file mode 100644 index 0b670bf99db..00000000000 --- a/sphinx/writers/_html4.py +++ /dev/null @@ -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('' % node['docname']) - - def depart_start_of_file(self, node: Element) -> None: - self.docnames.pop() - - ############################################################# - # Domain-specific object descriptions - ############################################################# - - # Top-level nodes for descriptions - ################################## - - def visit_desc(self, node: Element) -> None: - self.body.append(self.starttag(node, 'dl')) - - def depart_desc(self, node: Element) -> None: - self.body.append('\n\n') - - def visit_desc_signature(self, node: Element) -> None: - # the id is set automatically - self.body.append(self.starttag(node, 'dt')) - self.protect_literal_text += 1 - - def depart_desc_signature(self, node: Element) -> None: - self.protect_literal_text -= 1 - if not node.get('is_multiline'): - self.add_permalink_ref(node, _('Permalink to this definition')) - self.body.append('\n') - - def visit_desc_signature_line(self, node: Element) -> None: - pass - - def depart_desc_signature_line(self, node: Element) -> None: - if node.get('add_permalink'): - # the permalink info is on the parent desc_signature node - self.add_permalink_ref(node.parent, _('Permalink to this definition')) - self.body.append('
') - - def visit_desc_content(self, node: Element) -> None: - self.body.append(self.starttag(node, 'dd', '')) - - def depart_desc_content(self, node: Element) -> None: - self.body.append('') - - def visit_desc_inline(self, node: Element) -> None: - self.body.append(self.starttag(node, 'span', '')) - - def depart_desc_inline(self, node: Element) -> None: - self.body.append('') - - # Nodes for high-level structure in signatures - ############################################## - - def visit_desc_name(self, node: Element) -> None: - self.body.append(self.starttag(node, 'code', '')) - - def depart_desc_name(self, node: Element) -> None: - self.body.append('') - - def visit_desc_addname(self, node: Element) -> None: - self.body.append(self.starttag(node, 'code', '')) - - def depart_desc_addname(self, node: Element) -> None: - self.body.append('') - - def visit_desc_type(self, node: Element) -> None: - pass - - def depart_desc_type(self, node: Element) -> None: - pass - - def visit_desc_returns(self, node: Element) -> None: - self.body.append(' ') - self.body.append('') - self.body.append(' ') - - def depart_desc_returns(self, node: Element) -> None: - self.body.append('') - - def visit_desc_parameterlist(self, node: Element) -> None: - self.body.append('(') - 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(')') - - # 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('') - - def depart_desc_parameter(self, node: Element) -> None: - if not node.hasattr('noemph'): - self.body.append('') - 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('[') - - def depart_desc_optional(self, node: Element) -> None: - self.optional_param_level -= 1 - self.body.append(']') - - def visit_desc_annotation(self, node: Element) -> None: - self.body.append(self.starttag(node, 'em', '', CLASS='property')) - - def depart_desc_annotation(self, node: Element) -> None: - self.body.append('') - - ############################################## - - def visit_versionmodified(self, node: Element) -> None: - self.body.append(self.starttag(node, 'div', CLASS=node['type'])) - - def depart_versionmodified(self, node: Element) -> None: - self.body.append('\n') - - # overwritten - def visit_reference(self, node: Element) -> None: - atts = {'class': 'reference'} - if node.get('internal') or 'refuri' not in node: - atts['class'] += ' internal' - else: - atts['class'] += ' external' - if 'refuri' in node: - atts['href'] = node['refuri'] or '#' - if self.settings.cloak_email_addresses and atts['href'].startswith('mailto:'): - atts['href'] = self.cloak_mailto(atts['href']) - self.in_mailto = True - else: - assert 'refid' in node, \ - 'References must have "refuri" or "refid" attribute.' - atts['href'] = '#' + node['refid'] - if not isinstance(node.parent, nodes.TextElement): - assert len(node) == 1 and isinstance(node[0], nodes.image) # NoQA: PT018 - atts['class'] += ' image-reference' - if 'reftitle' in node: - atts['title'] = node['reftitle'] - if 'target' in node: - atts['target'] = node['target'] - self.body.append(self.starttag(node, 'a', '', **atts)) - - if node.get('secnumber'): - self.body.append(('%s' + self.secnumber_suffix) % - '.'.join(map(str, node['secnumber']))) - - def visit_number_reference(self, node: Element) -> None: - self.visit_reference(node) - - def depart_number_reference(self, node: Element) -> None: - self.depart_reference(node) - - # overwritten -- we don't want source comments to show up in the HTML - def visit_comment(self, node: Element) -> None: # type: ignore - 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('\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('%s' % - ('.'.join(map(str, secnumber)) + self.secnumber_suffix)) - - def add_fignumber(self, node: Element) -> None: - def append_fignumber(figtype: str, figure_id: str) -> None: - if self.builder.name == 'singlehtml': - key = f"{self.docnames[-1]}/{figtype}" - else: - key = figtype - - if figure_id in self.builder.fignumbers.get(key, {}): - self.body.append('') - prefix = self.config.numfig_format.get(figtype) - if prefix is None: - msg = __('numfig_format is not defined for %s') % figtype - logger.warning(msg) - else: - numbers = self.builder.fignumbers[key][figure_id] - self.body.append(prefix % '.'.join(map(str, numbers)) + ' ') - self.body.append('') - - figtype = self.builder.env.domains['std'].get_enumerable_node_type(node) - if figtype: - if len(node['ids']) == 0: - msg = __('Any IDs not assigned for %s node') % node.tagname - logger.warning(msg, location=node) - else: - append_fignumber(figtype, node['ids'][0]) - - def add_permalink_ref(self, node: Element, title: str) -> None: - if node['ids'] and self.config.html_permalinks and self.builder.add_permalinks: - format = '%s' - 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 (