mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Support `theme.toml
`
This commit is contained in:
parent
a5d7cce7f5
commit
8976fa4498
@ -30,11 +30,82 @@ Creating themes
|
||||
Themes take the form of either a directory or a zipfile (whose name is the
|
||||
theme name), containing the following:
|
||||
|
||||
* A :file:`theme.conf` file.
|
||||
* Either a :file:`theme.toml` file (preferred) or a :file:`theme.conf` file.
|
||||
* HTML templates, if needed.
|
||||
* A ``static/`` directory containing any static files that will be copied to the
|
||||
output static directory on build. These can be images, styles, script files.
|
||||
|
||||
Theme configuration (``theme.toml``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The :file:`theme.toml` file is a TOML_ document,
|
||||
containing two tables: ``[theme]`` and ``[options]``.
|
||||
|
||||
The ``[theme]`` table defines the theme's settings:
|
||||
|
||||
* **inherit** (string): The name of the base theme from which to inherit
|
||||
settings, options, templates, and static files.
|
||||
All static files from theme 'ancestors' will be used.
|
||||
The theme will use all options defined in inherited themes.
|
||||
Finally, inherited themes will be used to locate missing templates
|
||||
(for example, if ``"basic"`` is used as the base theme, most templates will
|
||||
already be defined).
|
||||
|
||||
If set to ``"none"``, the theme will not inherit from any other theme.
|
||||
Inheritance is recursive, forming a chain of inherited themes
|
||||
(e.g. ``default`` -> ``classic`` -> ``basic`` -> ``none``).
|
||||
|
||||
* **stylesheets** (list of strings): A list of CSS filenames which will be
|
||||
included in generated HTML header.
|
||||
Setting the :confval:`html_style` config value will override this setting.
|
||||
|
||||
Other mechanisms for including multiple stylesheets include ``@import`` in CSS
|
||||
or using a custom HTML template with appropriate ``<link rel="stylesheet">`` tags.
|
||||
|
||||
* **sidebars** (list of strings): A list of sidebar templates.
|
||||
This can be overridden by the user via the :confval:`html_sidebars` config value.
|
||||
|
||||
* **pygments_style** (table): A TOML table defining the names of Pygments styles
|
||||
to use for highlighting syntax.
|
||||
The table has two recognised keys: ``default`` and ``dark``.
|
||||
The style defined in the ``dark`` key will be used when
|
||||
the CSS media query ``(prefers-color-scheme: dark)`` evaluates to true.
|
||||
|
||||
``[theme.pygments_style.default]`` can be overridden by the user via the
|
||||
:confval:`pygments_style` config value.
|
||||
|
||||
The ``[options]`` table defines the options for the theme.
|
||||
It is structured such that each key-value pair corresponds to a variable name
|
||||
and the corresponding default value.
|
||||
These options can be overridden by the user in :confval:`html_theme_options`
|
||||
and are accessible from all templates as ``theme_<name>``.
|
||||
|
||||
.. versionadded:: 7.3
|
||||
``theme.toml`` support.
|
||||
|
||||
.. _TOML: https://toml.io/en/
|
||||
|
||||
Exemplar :file:`theme.toml` file:
|
||||
|
||||
.. code-block:: toml
|
||||
|
||||
[theme]
|
||||
inherit = "basic"
|
||||
stylesheets = [
|
||||
"main-CSS-stylesheet.css",
|
||||
]
|
||||
sidebars = [
|
||||
"localtoc.html",
|
||||
"relations.html",
|
||||
"sourcelink.html",
|
||||
"searchbox.html",
|
||||
]
|
||||
# Style names from https://pygments.org/styles/
|
||||
pygments_style = { default = "style_name", dark = "dark_style" }
|
||||
|
||||
[options]
|
||||
variable = "default value"
|
||||
|
||||
Theme configuration (``theme.conf``)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
@ -56,7 +56,7 @@ page's top and bottom), add the following :file:`conf.py`::
|
||||
|
||||
If the theme does not come with Sphinx, it can be in two static forms or as a
|
||||
Python package. For the static forms, either a directory (containing
|
||||
:file:`theme.conf` and other needed files), or a zip file with the same
|
||||
:file:`theme.toml` and other needed files), or a zip file with the same
|
||||
contents is supported. The directory or zipfile must be put where Sphinx can
|
||||
find it; for this there is the config value :confval:`html_theme_path`. This
|
||||
can be a list of directories, relative to the directory containing
|
||||
|
@ -21,17 +21,42 @@ from sphinx.locale import __
|
||||
from sphinx.util import logging
|
||||
from sphinx.util.osutil import ensuredir
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
import tomllib
|
||||
else:
|
||||
import tomli as tomllib # type: ignore[import-not-found]
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
from importlib.metadata import entry_points
|
||||
else:
|
||||
from importlib_metadata import entry_points # type: ignore[import-not-found]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import TypedDict
|
||||
|
||||
from typing_extensions import Required
|
||||
|
||||
from sphinx.application import Sphinx
|
||||
|
||||
class _ThemeToml(TypedDict, total=False):
|
||||
theme: Required[_ThemeTomlTheme]
|
||||
options: dict[str, str]
|
||||
|
||||
class _ThemeTomlTheme(TypedDict, total=False):
|
||||
inherit: Required[str]
|
||||
stylesheets: list[str]
|
||||
sidebars: list[str]
|
||||
pygments_style: _ThemeTomlThemePygments
|
||||
|
||||
class _ThemeTomlThemePygments(TypedDict, total=False):
|
||||
default: str
|
||||
dark: str
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_NO_DEFAULT = object()
|
||||
_THEME_TOML = 'theme.toml'
|
||||
_THEME_CONF = 'theme.conf'
|
||||
|
||||
|
||||
@ -185,7 +210,9 @@ class HTMLThemeFactory:
|
||||
entry,
|
||||
)
|
||||
else:
|
||||
if path.isfile(path.join(pathname, _THEME_CONF)):
|
||||
toml_path = path.join(pathname, _THEME_TOML)
|
||||
conf_path = path.join(pathname, _THEME_CONF)
|
||||
if path.isfile(toml_path) or path.isfile(conf_path):
|
||||
themes[entry] = pathname
|
||||
|
||||
return themes
|
||||
@ -196,7 +223,7 @@ class HTMLThemeFactory:
|
||||
self._load_extra_theme(name)
|
||||
|
||||
if name not in self._themes:
|
||||
raise ThemeError(__('no theme named %r found (missing theme.conf?)') % name)
|
||||
raise ThemeError(__('no theme named %r found (missing theme.toml?)') % name)
|
||||
|
||||
themes, theme_dirs, tmp_dirs = _load_theme_with_ancestors(self._themes, name)
|
||||
return Theme(name, configs=themes, paths=theme_dirs, tmp_dirs=tmp_dirs)
|
||||
@ -206,7 +233,8 @@ def _is_archived_theme(filename: str, /) -> bool:
|
||||
"""Check whether the specified file is an archived theme file or not."""
|
||||
try:
|
||||
with ZipFile(filename) as f:
|
||||
return _THEME_CONF in f.namelist()
|
||||
namelist = frozenset(f.namelist())
|
||||
return _THEME_TOML in namelist or _THEME_CONF in namelist
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@ -255,7 +283,11 @@ def _load_theme(name: str, theme_path: str, /) -> tuple[str, str, str | None, _C
|
||||
theme_dir = path.join(tmp_dir, name)
|
||||
_extract_zip(theme_path, theme_dir)
|
||||
|
||||
if path.isfile(conf_path := path.join(theme_dir, _THEME_CONF)):
|
||||
if path.isfile(toml_path := path.join(theme_dir, _THEME_TOML)):
|
||||
_cfg_table = _load_theme_toml(toml_path)
|
||||
inherit = _validate_theme_toml(_cfg_table, name)
|
||||
config = _convert_theme_toml(_cfg_table)
|
||||
elif path.isfile(conf_path := path.join(theme_dir, _THEME_CONF)):
|
||||
_cfg_parser = _load_theme_conf(conf_path)
|
||||
inherit = _validate_theme_conf(_cfg_parser, name)
|
||||
config = _convert_theme_conf(_cfg_parser)
|
||||
@ -279,6 +311,48 @@ def _extract_zip(filename: str, target_dir: str, /) -> None:
|
||||
fp.write(archive.read(name))
|
||||
|
||||
|
||||
def _load_theme_toml(config_file_path: str, /) -> _ThemeToml:
|
||||
with open(config_file_path, encoding='utf-8') as f:
|
||||
config_text = f.read()
|
||||
c = tomllib.loads(config_text)
|
||||
return {s: c[s] for s in ('theme', 'options') if s in c} # type: ignore[return-value]
|
||||
|
||||
|
||||
def _validate_theme_toml(cfg: _ThemeToml, name: str) -> str:
|
||||
if 'theme' not in cfg:
|
||||
raise ThemeError(__('theme %r doesn\'t have the "theme" table') % name)
|
||||
if inherit := cfg['theme'].get('inherit', ''):
|
||||
return inherit
|
||||
msg = __('The %r theme must define the "theme.inherit" setting') % name
|
||||
raise ThemeError(msg)
|
||||
|
||||
|
||||
def _convert_theme_toml(cfg: _ThemeToml, /) -> _ConfigFile:
|
||||
theme = cfg['theme']
|
||||
if 'stylesheets' in theme:
|
||||
stylesheets: tuple[str, ...] | None = tuple(theme['stylesheets'])
|
||||
else:
|
||||
stylesheets = None
|
||||
if 'sidebars' in theme:
|
||||
sidebar_templates: tuple[str, ...] | None = tuple(theme['sidebars'])
|
||||
else:
|
||||
sidebar_templates = None
|
||||
pygments_table = theme.get('pygments_style', {})
|
||||
if isinstance(pygments_table, str):
|
||||
hint = f'pygments_style = {{ default = "{pygments_table}" }}'
|
||||
msg = __('The "theme.pygments_style" setting must be a table. Hint: "%s"') % hint
|
||||
raise ThemeError(msg)
|
||||
pygments_style_default: str | None = pygments_table.get('default')
|
||||
pygments_style_dark: str | None = pygments_table.get('dark')
|
||||
return _ConfigFile(
|
||||
stylesheets=stylesheets,
|
||||
sidebar_templates=sidebar_templates,
|
||||
pygments_style_default=pygments_style_default,
|
||||
pygments_style_dark=pygments_style_dark,
|
||||
options=cfg.get('options', {}),
|
||||
)
|
||||
|
||||
|
||||
def _load_theme_conf(config_file_path: str, /) -> configparser.RawConfigParser:
|
||||
c = configparser.RawConfigParser()
|
||||
c.read(config_file_path, encoding='utf-8')
|
||||
|
@ -77,8 +77,8 @@ def test_theme_api(app, status, warning):
|
||||
assert not any(map(os.path.exists, theme._tmp_dirs))
|
||||
|
||||
|
||||
def test_nonexistent_theme_conf(tmp_path):
|
||||
# Check that error occurs with a non-existent theme.conf
|
||||
def test_nonexistent_theme_settings(tmp_path):
|
||||
# Check that error occurs with a non-existent theme.toml or theme.conf
|
||||
# (https://github.com/sphinx-doc/sphinx/issues/11668)
|
||||
with pytest.raises(ThemeError):
|
||||
_load_theme('', str(tmp_path))
|
||||
|
Loading…
Reference in New Issue
Block a user