"""Templates utility functions for Sphinx.""" from __future__ import annotations import os from functools import partial from os import path from typing import TYPE_CHECKING, Any, Callable 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 Sequence from jinja2.environment import Environment 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, context: dict[str, Any], ) -> str: dirname = os.path.dirname(filename) basename = os.path.basename(filename) return cls(dirname).render(basename, context) class SphinxRenderer(FileRenderer): def __init__(self, template_path: Sequence[str | os.PathLike[str]] | None = None) -> None: if template_path is None: template_path = os.path.join(package_dir, 'templates') super().__init__(template_path) @classmethod def render_from_file( cls: type[FileRenderer], filename: 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 = [os.path.join(package_dir, 'templates', 'latex')] 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 = [] for templates_path in templates_paths: loader = SphinxFileSystemLoader(path.join(confdir, 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]: 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)