"""
sphinx.builders.html
~~~~~~~~~~~~~~~~~~~~
Several HTML builders.
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
:license: BSD, see LICENSE for details.
"""
import html
import posixpath
import re
import sys
import warnings
from hashlib import md5
from os import path
from typing import Any, Dict, IO, Iterable, Iterator, List, Set, Tuple
from docutils import nodes
from docutils.core import publish_parts
from docutils.frontend import OptionParser
from docutils.io import DocTreeInput, StringOutput
from docutils.nodes import Node
from docutils.utils import relative_path
from sphinx import package_dir, __display_version__
from sphinx.application import Sphinx
from sphinx.builders import Builder
from sphinx.config import Config
from sphinx.deprecation import RemovedInSphinx30Warning, RemovedInSphinx40Warning
from sphinx.domains import Domain, Index, IndexEntry
from sphinx.environment.adapters.asset import ImageAdapter
from sphinx.environment.adapters.indexentries import IndexEntries
from sphinx.environment.adapters.toctree import TocTree
from sphinx.errors import ConfigError, ThemeError
from sphinx.highlighting import PygmentsBridge
from sphinx.locale import _, __
from sphinx.search import js_index
from sphinx.theming import HTMLThemeFactory
from sphinx.util import logging, progress_message, status_iterator
from sphinx.util.docutils import is_html5_writer_available, new_document
from sphinx.util.fileutil import copy_asset
from sphinx.util.i18n import format_date
from sphinx.util.inventory import InventoryFile
from sphinx.util.matching import patmatch, Matcher, DOTFILES
from sphinx.util.osutil import os_path, relative_uri, ensuredir, movefile, copyfile
from sphinx.util.tags import Tags
from sphinx.writers.html import HTMLWriter, HTMLTranslator
if False:
# For type annotation
from typing import Type # for python3.5.1
# HTML5 Writer is avialable or not
if is_html5_writer_available():
from sphinx.writers.html5 import HTML5Translator
html5_ready = True
else:
html5_ready = False
#: the filename for the inventory of objects
INVENTORY_FILENAME = 'objects.inv'
logger = logging.getLogger(__name__)
return_codes_re = re.compile('[\r\n]+')
def get_stable_hash(obj: Any) -> str:
"""
Return a stable hash for a Python data structure. We can't just use
the md5 of str(obj) since for example dictionary items are enumerated
in unpredictable order due to hash randomization in newer Pythons.
"""
if isinstance(obj, dict):
return get_stable_hash(list(obj.items()))
elif isinstance(obj, (list, tuple)):
obj = sorted(get_stable_hash(o) for o in obj)
return md5(str(obj).encode()).hexdigest()
class Stylesheet(str):
"""A metadata of stylesheet.
To keep compatibility with old themes, an instance of stylesheet behaves as
its filename (str).
"""
attributes = None # type: Dict[str, str]
filename = None # type: str
def __new__(cls, filename: str, *args: str, **attributes: str) -> "Stylesheet":
self = str.__new__(cls, filename) # type: ignore
self.filename = filename
self.attributes = attributes
self.attributes.setdefault('rel', 'stylesheet')
self.attributes.setdefault('type', 'text/css')
if args: # old style arguments (rel, title)
self.attributes['rel'] = args[0]
self.attributes['title'] = args[1]
return self
class JSContainer(list):
"""The container for JavaScript scripts."""
def insert(self, index: int, obj: str) -> None:
warnings.warn('To modify script_files in the theme is deprecated. '
'Please insert a ' % (' '.join(attrs), body)
context['js_tag'] = js_tag
def validate_math_renderer(app: Sphinx) -> None:
if app.builder.format != 'html':
return
name = app.builder.math_renderer_name # type: ignore
if name is None:
raise ConfigError(__('Many math_renderers are registered. '
'But no math_renderer is selected.'))
elif name not in app.registry.html_inline_math_renderers:
raise ConfigError(__('Unknown math_renderer %r is given.') % name)
def validate_html_extra_path(app: Sphinx, config: Config) -> None:
"""Check html_extra_paths setting."""
for entry in config.html_extra_path[:]:
extra_path = path.normpath(path.join(app.confdir, entry))
if not path.exists(extra_path):
logger.warning(__('html_extra_path entry %r does not exist'), entry)
config.html_extra_path.remove(entry)
elif (path.splitdrive(app.outdir)[0] == path.splitdrive(extra_path)[0] and
path.commonpath([app.outdir, extra_path]) == app.outdir):
logger.warning(__('html_extra_path entry %r is placed inside outdir'), entry)
config.html_extra_path.remove(entry)
def validate_html_static_path(app: Sphinx, config: Config) -> None:
"""Check html_static_paths setting."""
for entry in config.html_static_path[:]:
static_path = path.normpath(path.join(app.confdir, entry))
if not path.exists(static_path):
logger.warning(__('html_static_path entry %r does not exist'), entry)
config.html_static_path.remove(entry)
elif (path.splitdrive(app.outdir)[0] == path.splitdrive(static_path)[0] and
path.commonpath([app.outdir, static_path]) == app.outdir):
logger.warning(__('html_static_path entry %r is placed inside outdir'), entry)
config.html_static_path.remove(entry)
def validate_html_logo(app: Sphinx, config: Config) -> None:
"""Check html_logo setting."""
if config.html_logo and not path.isfile(path.join(app.confdir, config.html_logo)):
logger.warning(__('logo file %r does not exist'), config.html_logo)
config.html_logo = None # type: ignore
def validate_html_favicon(app: Sphinx, config: Config) -> None:
"""Check html_favicon setting."""
if config.html_favicon and not path.isfile(path.join(app.confdir, config.html_favicon)):
logger.warning(__('favicon file %r does not exist'), config.html_favicon)
config.html_favicon = None # type: ignore
# for compatibility
import sphinx.builders.dirhtml # NOQA
import sphinx.builders.singlehtml # NOQA
import sphinxcontrib.serializinghtml # NOQA
def setup(app: Sphinx) -> Dict[str, Any]:
# builders
app.add_builder(StandaloneHTMLBuilder)
# config values
app.add_config_value('html_theme', 'alabaster', 'html')
app.add_config_value('html_theme_path', [], 'html')
app.add_config_value('html_theme_options', {}, 'html')
app.add_config_value('html_title',
lambda self: _('%s %s documentation') % (self.project, self.release),
'html', [str])
app.add_config_value('html_short_title', lambda self: self.html_title, 'html')
app.add_config_value('html_style', None, 'html', [str])
app.add_config_value('html_logo', None, 'html', [str])
app.add_config_value('html_favicon', None, 'html', [str])
app.add_config_value('html_css_files', [], 'html')
app.add_config_value('html_js_files', [], 'html')
app.add_config_value('html_static_path', [], 'html')
app.add_config_value('html_extra_path', [], 'html')
app.add_config_value('html_last_updated_fmt', None, 'html', [str])
app.add_config_value('html_sidebars', {}, 'html')
app.add_config_value('html_additional_pages', {}, 'html')
app.add_config_value('html_domain_indices', True, 'html', [list])
app.add_config_value('html_add_permalinks', 'ΒΆ', 'html')
app.add_config_value('html_use_index', True, 'html')
app.add_config_value('html_split_index', False, 'html')
app.add_config_value('html_copy_source', True, 'html')
app.add_config_value('html_show_sourcelink', True, 'html')
app.add_config_value('html_sourcelink_suffix', '.txt', 'html')
app.add_config_value('html_use_opensearch', '', 'html')
app.add_config_value('html_file_suffix', None, 'html', [str])
app.add_config_value('html_link_suffix', None, 'html', [str])
app.add_config_value('html_show_copyright', True, 'html')
app.add_config_value('html_show_sphinx', True, 'html')
app.add_config_value('html_context', {}, 'html')
app.add_config_value('html_output_encoding', 'utf-8', 'html')
app.add_config_value('html_compact_lists', True, 'html')
app.add_config_value('html_secnumber_suffix', '. ', 'html')
app.add_config_value('html_search_language', None, 'html', [str])
app.add_config_value('html_search_options', {}, 'html')
app.add_config_value('html_search_scorer', '', None)
app.add_config_value('html_scaled_image_link', True, 'html')
app.add_config_value('html_baseurl', '', 'html')
app.add_config_value('html_math_renderer', None, 'env')
app.add_config_value('html4_writer', False, 'html')
# event handlers
app.connect('config-inited', convert_html_css_files)
app.connect('config-inited', convert_html_js_files)
app.connect('config-inited', validate_html_extra_path)
app.connect('config-inited', validate_html_static_path)
app.connect('config-inited', validate_html_logo)
app.connect('config-inited', validate_html_favicon)
app.connect('builder-inited', validate_math_renderer)
app.connect('html-page-context', setup_js_tag_helper)
# load default math renderer
app.setup_extension('sphinx.ext.mathjax')
return {
'version': 'builtin',
'parallel_read_safe': True,
'parallel_write_safe': True,
}