"""The Sphinx awesome theme as a Python package.
:copyright: Copyright Kai Welke.
:license: MIT, see LICENSE for details
"""
from __future__ import annotations
from dataclasses import dataclass, field
from importlib.metadata import PackageNotFoundError, version
from pathlib import Path
from typing import Any, TypedDict
from docutils.parsers.rst import directives
from sphinx.application import Sphinx
from sphinx.builders.html import StandaloneHTMLBuilder
from sphinx.util import logging
from sphinxcontrib.serializinghtml import JSONHTMLBuilder
from . import jsonimpl
from .builder import AwesomeHTMLBuilder
from .code import AwesomeCodeBlock
from .jinja_functions import setup_jinja
from .logos import copy_logos, get_theme_options, setup_logo_path, update_config
from .postprocess import changed_docs, post_process_html
from .toc import change_toc
logger = logging.getLogger(__name__)
try:
# obtain version from `pyproject.toml` via `importlib.metadata.version()`
__version__ = version(__name__)
except PackageNotFoundError: # pragma: no cover
__version__ = "unknown"
[docs]class LinkIcon(TypedDict):
"""A link to an external resource, represented by an icon."""
link: str
"""The absolute URL to an external resource."""
icon: str
"""An SVG icon as a string."""
[docs]@dataclass
class ThemeOptions:
"""Helper class for configuring the Awesome Theme.
Each attribute becomes a key in the :confval:`sphinx:html_theme_options` dictionary.
"""
show_prev_next: bool = True
"""If ``True``, the theme includes links to the previous and next pages in the hierarchy."""
show_breadcrumbs: bool = True
"""If ``True``, the theme includes `breadcrumbs <https://en.wikipedia.org/wiki/Breadcrumb_navigation>`_ links on every page except the root page."""
breadcrumbs_separator: str = "/"
"""The separator for the breadcrumbs links."""
awesome_headerlinks: bool = True
"""If ``True``, clicking a headerlink copies its URL to the clipboard."""
show_scrolltop: bool = False
"""If ``True``, the theme shows a button that scrolls to the top of the page when clicked."""
awesome_external_links: bool = False
"""If ``True``, the theme includes an icon after external links and adds ``rel="nofollow noopener"`` to the links' attributes."""
main_nav_links: dict[str, str] = field(default_factory=dict)
"""A dictionary with links to include in the site header.
The keys of this dictionary are the labels for the links.
The values are absolute or relative URLs.
"""
extra_header_link_icons: dict[str, LinkIcon] = field(default_factory=dict)
"""A dictionary with extra icons to include on the right of the search bar.
The keys are labels for the link's ``title`` attribute.
The values are themselves :class:`LinkIcon`.
"""
logo_light: str | None = None
"""A path to a logo for the light mode. If you're using separate logos for light and dark mode, you **must** provide both logos."""
logo_dark: str | None = None
"""A path to a logo for the dark mode. If you're using separate logos for light and dark mode, you **must** provide both logos."""
globaltoc_includehidden: bool = True
"""If ``True``, the theme includes entries from *hidden*
:sphinxdocs:`toctree <usage/restructuredtext/directives.html#directive-toctree>` directives in the sidebar.
The ``toctree`` directive generates a list of links on the page where you include it,
unless you set the ``:hidden:`` option.
This option is inherited from the ``basic`` theme.
"""
nav_include_hidden: None = None
"""Deprecated. Use `globaltoc_includehidden` instead.
.. deprecated:: 5.0
"""
show_nav: None = None
"""Deprecated. Use the `html_sidebars` option instead.
.. deprecated:: 5.0
"""
extra_header_links: None = None
"""Deprecated. Use either `main_nav_links` or `extra_header_link_icons` instead.
.. deprecated:: 5.0
"""
def deprecated_options(app: Sphinx) -> None:
"""Checks for deprecated ``html_theme_options``.
Raises warnings and set the correct options.
"""
theme_options = get_theme_options(app)
if (
"nav_include_hidden" in theme_options
and theme_options["nav_include_hidden"] is not None
):
logger.warning(
"Setting `nav_include_hidden` in `html_theme_options` is deprecated. "
"Use `globaltoc_includehidden` in `html_theme_options` instead."
)
theme_options["globaltoc_includehidden"] = theme_options["nav_include_hidden"]
del theme_options["nav_include_hidden"]
if "show_nav" in theme_options and theme_options["show_nav"] is not None:
logger.warning(
"Toggling the sidebar with `show_nav` in `html_theme_options` is deprecated. "
"Control the sidebar with the `html_sidebars` configuration option instead."
)
if theme_options["show_nav"] is False:
app.builder.config.html_sidebars = {"**": []} # type: ignore[attr-defined]
del theme_options["show_nav"]
if (
"extra_header_links" in theme_options
and theme_options["extra_header_links"] is not None
):
logger.warning(
"`extra_header_links` is deprecated. "
"Use `main_nav_links` for text links (left side) and `extra_header_link_icons` for icon links (right side) instead."
)
extra_links = theme_options["extra_header_links"]
# Either we have `extra_header_links = { "label": "url" }
main_nav_links = {
key: value for key, value in extra_links.items() if isinstance(value, str)
}
theme_options["main_nav_links"] = main_nav_links
# Or we have `extra_header_links` = { "label": { "link": "link", "icon": "icon" }}
extra_link_icons = {
key: value for key, value in extra_links.items() if isinstance(value, dict)
}
theme_options["extra_header_link_icons"] = extra_link_icons
del theme_options["extra_header_links"]
def setup(app: Sphinx) -> dict[str, Any]:
"""Register the theme and its extensions wih Sphinx."""
here = Path(__file__).parent.resolve()
directives.register_directive("code-block", AwesomeCodeBlock)
app.add_config_value("pygments_style_dark", None, "html", [str])
# Monkey-patch galore
StandaloneHTMLBuilder.init_highlighter = AwesomeHTMLBuilder.init_highlighter # type: ignore
StandaloneHTMLBuilder.create_pygments_style_file = ( # type: ignore
AwesomeHTMLBuilder.create_pygments_style_file # type: ignore
)
app.add_html_theme(name="sphinxawesome_theme", theme_path=str(here))
app.add_js_file("theme.js", loading_method="defer")
# Add the CSS overrides if we're using the `sphinx-design` extension
if "sphinx_design" in app.config.extensions:
app.add_css_file("awesome-sphinx-design.css", priority=900)
if "sphinx_docsearch" in app.config.extensions:
app.add_css_file("awesome-docsearch.css", priority=900)
app.config.html_context.update({"docsearch": True})
# The theme is set up _after_ extensions are set up,
# so I can't use internal extensions.
# For the same reason, I also can't call the `config-inited` event
app.connect("builder-inited", deprecated_options)
app.connect("builder-inited", update_config)
app.connect("html-page-context", setup_logo_path)
app.connect("html-page-context", setup_jinja)
app.connect("html-page-context", change_toc)
app.connect("build-finished", copy_logos)
app.connect("env-before-read-docs", changed_docs)
app.connect("build-finished", post_process_html)
JSONHTMLBuilder.out_suffix = ".json"
JSONHTMLBuilder.implementation = jsonimpl
JSONHTMLBuilder.indexer_format = jsonimpl
return {
"version": __version__,
"parallel_read_safe": True,
"parallel_write_safe": True,
}