summaryrefslogtreecommitdiffstats
path: root/sphinx/jinja2glue.py
diff options
context:
space:
mode:
Diffstat (limited to 'sphinx/jinja2glue.py')
-rw-r--r--sphinx/jinja2glue.py221
1 files changed, 221 insertions, 0 deletions
diff --git a/sphinx/jinja2glue.py b/sphinx/jinja2glue.py
new file mode 100644
index 0000000..cfe92b0
--- /dev/null
+++ b/sphinx/jinja2glue.py
@@ -0,0 +1,221 @@
+"""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)