You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
6.9 KiB
214 lines
6.9 KiB
4 years ago
|
"""
|
||
|
sphinx.jinja2glue
|
||
|
~~~~~~~~~~~~~~~~~
|
||
|
|
||
|
Glue code for the jinja2 templating engine.
|
||
|
|
||
|
:copyright: Copyright 2007-2020 by the Sphinx team, see AUTHORS.
|
||
|
:license: BSD, see LICENSE for details.
|
||
|
"""
|
||
|
|
||
|
from os import path
|
||
|
from pprint import pformat
|
||
|
from typing import Any, Callable, Dict, Iterator, List, Tuple, Union
|
||
|
|
||
|
from jinja2 import BaseLoader, FileSystemLoader, TemplateNotFound, contextfunction
|
||
|
from jinja2.environment import Environment
|
||
|
from jinja2.sandbox import SandboxedEnvironment
|
||
|
from jinja2.utils import open_if_exists
|
||
|
|
||
|
from sphinx.application import TemplateBridge
|
||
|
from sphinx.theming import Theme
|
||
|
from sphinx.util import logging
|
||
|
from sphinx.util.osutil import mtimes_of_files
|
||
|
|
||
|
if False:
|
||
|
# For type annotation
|
||
|
from sphinx.builders import Builder
|
||
|
|
||
|
|
||
|
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: Union[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
|
||
|
|
||
|
|
||
|
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)
|
||
|
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
|
||
|
|
||
|
|
||
|
@contextfunction
|
||
|
def warning(context: Dict, message: str, *args: Any, **kwargs: Any) -> str:
|
||
|
if 'pagename' in context:
|
||
|
filename = context.get('pagename') + context.get('file_suffix', '')
|
||
|
message = 'in rendering %s: %s' % (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 None:
|
||
|
continue
|
||
|
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
|
||
|
raise TemplateNotFound(template)
|
||
|
|
||
|
|
||
|
class BuiltinTemplateLoader(TemplateBridge, BaseLoader):
|
||
|
"""
|
||
|
Interfaces the rendering environment of jinja2 for use in Sphinx.
|
||
|
"""
|
||
|
|
||
|
# TemplateBridge interface
|
||
|
|
||
|
def init(self, builder: "Builder", theme: Theme = None, dirs: List[str] = 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'] = contextfunction(pformat)
|
||
|
self.environment.globals['warning'] = warning
|
||
|
self.environment.globals['accesskey'] = contextfunction(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
|
||
|
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)
|