diff options
author | Adam Turner <9087854+AA-Turner@users.noreply.github.com> | 2023-04-28 11:32:12 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-04-28 11:32:12 +0100 |
commit | ad473730a3763f241e9aaea1a87d1893f01b86fd (patch) | |
tree | 84da748a87f2b4c3f2274a770697bb322a718578 | |
parent | 3e3251d3ba4f1e5fa6d34d995505f4d3ace5acad (diff) | |
download | sphinx-git-ad473730a3763f241e9aaea1a87d1893f01b86fd.tar.gz |
Remove HTML 4 support (#11385)
-rw-r--r-- | sphinx/builders/html/__init__.py | 22 | ||||
-rw-r--r-- | sphinx/themes/basic/layout.html | 16 | ||||
-rw-r--r-- | sphinx/writers/_html4.py | 857 | ||||
-rw-r--r-- | sphinx/writers/html.py | 7 | ||||
-rw-r--r-- | tests/test_build_html.py | 22 |
5 files changed, 25 insertions, 899 deletions
diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index cd6e9498e..8b8c426a4 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -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) diff --git a/sphinx/themes/basic/layout.html b/sphinx/themes/basic/layout.html index 6e9096a1d..f3088f79a 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 -%} <!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 ' »' 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 %} diff --git a/sphinx/writers/_html4.py b/sphinx/writers/_html4.py deleted file mode 100644 index 0b670bf99..000000000 --- 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('<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">→</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(' ', ' ') - - # overwritten - def visit_Text(self, node: Text) -> None: - text = node.astext() - encoded = self.encode(text) - if self.protect_literal_text: - # moved here from base class's visit_literal to support - # more formatting in literal nodes - for token in self.words_and_spaces.findall(encoded): - if token.strip(): - # protect literal text from line wrapping - self.body.append('<span class="pre">%s</span>' % token) - elif token in ' \n': - # allow breaks at whitespace - self.body.append(token) - else: - # protect runs of multiple spaces; the last one can wrap - self.body.append(' ' * (len(token) - 1) + ' ') - else: - if self.in_mailto and self.settings.cloak_email_addresses: - encoded = self.cloak_email(encoded) - self.body.append(encoded) - - def visit_note(self, node: Element) -> None: - self.visit_admonition(node, 'note') - - def depart_note(self, node: Element) -> None: - self.depart_admonition(node) - - def visit_warning(self, node: Element) -> None: - self.visit_admonition(node, 'warning') - - def depart_warning(self, node: Element) -> None: - self.depart_admonition(node) - - def visit_attention(self, node: Element) -> None: - self.visit_admonition(node, 'attention') - - def depart_attention(self, node: Element) -> None: - self.depart_admonition(node) - - def visit_caution(self, node: Element) -> None: - self.visit_admonition(node, 'caution') - - def depart_caution(self, node: Element) -> None: - self.depart_admonition(node) - - def visit_danger(self, node: Element) -> None: - self.visit_admonition(node, 'danger') - - def depart_danger(self, node: Element) -> None: - self.depart_admonition(node) - - def visit_error(self, node: Element) -> None: - self.visit_admonition(node, 'error') - - def depart_error(self, node: Element) -> None: - self.depart_admonition(node) - - def visit_hint(self, node: Element) -> None: - self.visit_admonition(node, 'hint') - - def depart_hint(self, node: Element) -> None: - self.depart_admonition(node) - - def visit_important(self, node: Element) -> None: - self.visit_admonition(node, 'important') - - def depart_important(self, node: Element) -> None: - self.depart_admonition(node) - - def visit_tip(self, node: Element) -> None: - self.visit_admonition(node, 'tip') - - def depart_tip(self, node: Element) -> None: - self.depart_admonition(node) - - def visit_literal_emphasis(self, node: Element) -> None: - return self.visit_emphasis(node) - - def depart_literal_emphasis(self, node: Element) -> None: - return self.depart_emphasis(node) - - def visit_literal_strong(self, node: Element) -> None: - return self.visit_strong(node) - - def depart_literal_strong(self, node: Element) -> None: - return self.depart_strong(node) - - def visit_abbreviation(self, node: Element) -> None: - attrs = {} - if node.hasattr('explanation'): - attrs['title'] = node['explanation'] - self.body.append(self.starttag(node, 'abbr', '', **attrs)) - - def depart_abbreviation(self, node: Element) -> None: - self.body.append('</abbr>') - - def visit_manpage(self, node: Element) -> None: - self.visit_literal_emphasis(node) - 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] == ' ': - self.body[-1] = ' ' - - 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(' ', ' ') - - def visit_math(self, node: Element, math_env: str = '') -> None: - name = self.builder.math_renderer_name - visit, _ = self.builder.app.registry.html_inline_math_renderers[name] - visit(self, node) - - def depart_math(self, node: Element, math_env: str = '') -> None: - name = self.builder.math_renderer_name - _, depart = self.builder.app.registry.html_inline_math_renderers[name] - if depart: # type: ignore[truthy-function] - depart(self, node) - - def visit_math_block(self, node: Element, math_env: str = '') -> None: - name = self.builder.math_renderer_name - visit, _ = self.builder.app.registry.html_block_math_renderers[name] - visit(self, node) - - def depart_math_block(self, node: Element, math_env: str = '') -> None: - name = self.builder.math_renderer_name - _, depart = self.builder.app.registry.html_block_math_renderers[name] - if depart: # type: ignore[truthy-function] - depart(self, node) diff --git a/sphinx/writers/html.py b/sphinx/writers/html.py index faa8d8ad8..198e03508 100644 --- a/sphinx/writers/html.py +++ b/sphinx/writers/html.py @@ -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', diff --git a/tests/test_build_html.py b/tests/test_build_html.py index 013c09bdb..8591897fb 100644 --- a/tests/test_build_html.py +++ b/tests/test_build_html.py @@ -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({ |