"""Glue code for the jinja2 templating engine.""" from __future__ import annotations from os import path from pprint import pformat from typing import TYPE_CHECKING, Any, Callable from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound from jinja2.sandbox import SandboxedEnvironment from jinja2.utils import open_if_exists from sphinx.application import TemplateBridge from sphinx.util import logging from sphinx.util.osutil import mtimes_of_files try: from jinja2.utils import pass_context except ImportError: from jinja2 import contextfunction as pass_context if TYPE_CHECKING: from collections.abc import Iterator from jinja2.environment import Environment from sphinx.builders import Builder from sphinx.theming import Theme def _tobool(val: str) -> bool: if isinstance(val, str): return val.lower() in ('true', '1', 'yes', 'on') return bool(val) def _toint(val: str) -> int: try: return int(val) except ValueError: return 0 def _todim(val: int | str) -> str: """ Make val a css dimension. In particular the following transformations are performed: - None -> 'initial' (default CSS value) - 0 -> '0' - ints and string representations of ints are interpreted as pixels. Everything else is returned unchanged. """ if val is None: return 'initial' elif str(val).isdigit(): return '0' if int(val) == 0 else '%spx' % val return val # type: ignore[return-value] def _slice_index(values: list, slices: int) -> Iterator[list]: seq = list(values) length = 0 for value in values: length += 1 + len(value[1][1]) # count includes subitems items_per_slice = length // slices offset = 0 for slice_number in range(slices): count = 0 start = offset if slices == slice_number + 1: # last column offset = len(seq) # noqa: SIM113 else: for value in values[offset:]: count += 1 + len(value[1][1]) offset += 1 if count >= items_per_slice: break yield seq[start:offset] def accesskey(context: Any, key: str) -> str: """Helper to output each access key only once.""" if '_accesskeys' not in context: context.vars['_accesskeys'] = {} if key and key not in context.vars['_accesskeys']: context.vars['_accesskeys'][key] = 1 return 'accesskey="%s"' % key return '' class idgen: def __init__(self) -> None: self.id = 0 def current(self) -> int: return self.id def __next__(self) -> int: self.id += 1 return self.id next = __next__ # Python 2/Jinja compatibility @pass_context def warning(context: dict, message: str, *args: Any, **kwargs: Any) -> str: if 'pagename' in context: filename = context.get('pagename') + context.get('file_suffix', '') message = f'in rendering {filename}: {message}' logger = logging.getLogger('sphinx.themes') logger.warning(message, *args, **kwargs) return '' # return empty string not to output any values class SphinxFileSystemLoader(FileSystemLoader): """ FileSystemLoader subclass that is not so strict about '..' entries in template names. """ def get_source(self, environment: Environment, template: str) -> tuple[str, str, Callable]: for searchpath in self.searchpath: filename = path.join(searchpath, template) f = open_if_exists(filename) if f is not None: break else: raise TemplateNotFound(template) with f: contents = f.read().decode(self.encoding) mtime = path.getmtime(filename) def uptodate() -> bool: try: return path.getmtime(filename) == mtime except OSError: return False return contents, filename, uptodate class BuiltinTemplateLoader(TemplateBridge, BaseLoader): """ Interfaces the rendering environment of jinja2 for use in Sphinx. """ # TemplateBridge interface def init( self, builder: Builder, theme: Theme | None = None, dirs: list[str] | None = None, ) -> None: # create a chain of paths to search if theme: # the theme's own dir and its bases' dirs pathchain = theme.get_theme_dirs() # the loader dirs: pathchain + the parent directories for all themes loaderchain = pathchain + [path.join(p, '..') for p in pathchain] elif dirs: pathchain = list(dirs) loaderchain = list(dirs) else: pathchain = [] loaderchain = [] # prepend explicit template paths self.templatepathlen = len(builder.config.templates_path) if builder.config.templates_path: cfg_templates_path = [path.join(builder.confdir, tp) for tp in builder.config.templates_path] pathchain[0:0] = cfg_templates_path loaderchain[0:0] = cfg_templates_path # store it for use in newest_template_mtime self.pathchain = pathchain # make the paths into loaders self.loaders = [SphinxFileSystemLoader(x) for x in loaderchain] use_i18n = builder.app.translator is not None extensions = ['jinja2.ext.i18n'] if use_i18n else [] self.environment = SandboxedEnvironment(loader=self, extensions=extensions) self.environment.filters['tobool'] = _tobool self.environment.filters['toint'] = _toint self.environment.filters['todim'] = _todim self.environment.filters['slice_index'] = _slice_index self.environment.globals['debug'] = pass_context(pformat) self.environment.globals['warning'] = warning self.environment.globals['accesskey'] = pass_context(accesskey) self.environment.globals['idgen'] = idgen if use_i18n: self.environment.install_gettext_translations(builder.app.translator) def render(self, template: str, context: dict) -> str: # type: ignore[override] return self.environment.get_template(template).render(context) def render_string(self, source: str, context: dict) -> str: return self.environment.from_string(source).render(context) def newest_template_mtime(self) -> float: return max(mtimes_of_files(self.pathchain, '.html')) # Loader interface def get_source(self, environment: Environment, template: str) -> tuple[str, str, Callable]: loaders = self.loaders # exclamation mark starts search from theme if template.startswith('!'): loaders = loaders[self.templatepathlen:] template = template[1:] for loader in loaders: try: return loader.get_source(environment, template) except TemplateNotFound: pass raise TemplateNotFound(template)