"""Templates utility functions for Sphinx."""
from __future__ import annotations
import os
from functools import partial
from pathlib import Path
from typing import TYPE_CHECKING
from jinja2 import TemplateNotFound
from jinja2.loaders import BaseLoader
from jinja2.sandbox import SandboxedEnvironment
from sphinx import package_dir
from sphinx.jinja2glue import SphinxFileSystemLoader
from sphinx.locale import get_translator
from sphinx.util import rst, texescape
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from typing import Any
from jinja2.environment import Environment
_TEMPLATES_PATH = package_dir / 'templates'
_LATEX_TEMPLATES_PATH = _TEMPLATES_PATH / 'latex'
class BaseRenderer:
def __init__(self, loader: BaseLoader | None = None) -> None:
self.env = SandboxedEnvironment(loader=loader, extensions=['jinja2.ext.i18n'])
self.env.filters['repr'] = repr
# ``install_gettext_translations`` is injected by the ``jinja2.ext.i18n`` extension
self.env.install_gettext_translations(get_translator()) # type: ignore[attr-defined]
def render(self, template_name: str, context: dict[str, Any]) -> str:
return self.env.get_template(template_name).render(context)
def render_string(self, source: str, context: dict[str, Any]) -> str:
return self.env.from_string(source).render(context)
class FileRenderer(BaseRenderer):
def __init__(self, search_path: Sequence[str | os.PathLike[str]]) -> None:
if isinstance(search_path, (str, os.PathLike)):
search_path = [search_path]
else:
# filter "None" paths
search_path = list(filter(None, search_path))
loader = SphinxFileSystemLoader(search_path)
super().__init__(loader)
@classmethod
def render_from_file(
cls: type[FileRenderer],
filename: str | os.PathLike[str],
context: dict[str, Any],
) -> str:
filename = Path(filename)
return cls((filename.parent,)).render(filename.name, context)
class SphinxRenderer(FileRenderer):
def __init__(
self, template_path: Sequence[str | os.PathLike[str]] | None = None
) -> None:
if template_path is None:
template_path = (_TEMPLATES_PATH,)
super().__init__(template_path)
@classmethod
def render_from_file(
cls: type[FileRenderer],
filename: str | os.PathLike[str],
context: dict[str, Any],
) -> str:
return FileRenderer.render_from_file(filename, context)
class LaTeXRenderer(SphinxRenderer):
def __init__(
self,
template_path: Sequence[str | os.PathLike[str]] | None = None,
latex_engine: str | None = None,
) -> None:
if template_path is None:
template_path = (_LATEX_TEMPLATES_PATH,)
super().__init__(template_path)
# use texescape as escape filter
escape = partial(texescape.escape, latex_engine=latex_engine)
self.env.filters['e'] = escape
self.env.filters['escape'] = escape
self.env.filters['eabbr'] = texescape.escape_abbr
# use JSP/eRuby like tagging instead because curly bracket; the default
# tagging of jinja2 is not good for LaTeX sources.
self.env.variable_start_string = '<%='
self.env.variable_end_string = '%>'
self.env.block_start_string = '<%'
self.env.block_end_string = '%>'
self.env.comment_start_string = '<#'
self.env.comment_end_string = '#>'
class ReSTRenderer(SphinxRenderer):
def __init__(
self,
template_path: Sequence[str | os.PathLike[str]] | None = None,
language: str | None = None,
) -> None:
super().__init__(template_path)
# add language to environment
self.env.extend(language=language)
# use texescape as escape filter
self.env.filters['e'] = rst.escape
self.env.filters['escape'] = rst.escape
self.env.filters['heading'] = rst.heading
class SphinxTemplateLoader(BaseLoader):
"""A loader supporting template inheritance"""
def __init__(
self,
confdir: str | os.PathLike[str],
templates_paths: Sequence[str | os.PathLike[str]],
system_templates_paths: Sequence[str | os.PathLike[str]],
) -> None:
self.loaders = []
self.sysloaders = []
conf_dir = Path(confdir)
for templates_path in templates_paths:
loader = SphinxFileSystemLoader(conf_dir / templates_path)
self.loaders.append(loader)
for templates_path in system_templates_paths:
loader = SphinxFileSystemLoader(templates_path)
self.loaders.append(loader)
self.sysloaders.append(loader)
def get_source(
self,
environment: Environment,
template: str,
) -> tuple[str, str, Callable[[], bool]]:
if template.startswith('!'):
# search a template from ``system_templates_paths``
loaders = self.sysloaders
template = template[1:]
else:
loaders = self.loaders
for loader in loaders:
try:
return loader.get_source(environment, template)
except TemplateNotFound:
pass
raise TemplateNotFound(template)