1"""
2    sphinx.util.template
3    ~~~~~~~~~~~~~~~~~~~~
4
5    Templates utility functions for Sphinx.
6
7    :copyright: Copyright 2007-2021 by the Sphinx team, see AUTHORS.
8    :license: BSD, see LICENSE for details.
9"""
10
11import os
12from functools import partial
13from os import path
14from typing import Callable, Dict, List, Tuple, Union
15
16from jinja2 import TemplateNotFound
17from jinja2.environment import Environment
18from jinja2.loaders import BaseLoader
19from jinja2.sandbox import SandboxedEnvironment
20
21from sphinx import package_dir
22from sphinx.jinja2glue import SphinxFileSystemLoader
23from sphinx.locale import get_translator
24from sphinx.util import rst, texescape
25
26
27class BaseRenderer:
28    def __init__(self, loader: BaseLoader = None) -> None:
29        self.env = SandboxedEnvironment(loader=loader, extensions=['jinja2.ext.i18n'])
30        self.env.filters['repr'] = repr
31        self.env.install_gettext_translations(get_translator())
32
33    def render(self, template_name: str, context: Dict) -> str:
34        return self.env.get_template(template_name).render(context)
35
36    def render_string(self, source: str, context: Dict) -> str:
37        return self.env.from_string(source).render(context)
38
39
40class FileRenderer(BaseRenderer):
41    def __init__(self, search_path: Union[str, List[str]]) -> None:
42        if isinstance(search_path, str):
43            search_path = [search_path]
44        else:
45            # filter "None" paths
46            search_path = list(filter(None, search_path))
47
48        loader = SphinxFileSystemLoader(search_path)
49        super().__init__(loader)
50
51    @classmethod
52    def render_from_file(cls, filename: str, context: Dict) -> str:
53        dirname = os.path.dirname(filename)
54        basename = os.path.basename(filename)
55        return cls(dirname).render(basename, context)
56
57
58class SphinxRenderer(FileRenderer):
59    def __init__(self, template_path: Union[str, List[str]] = None) -> None:
60        if template_path is None:
61            template_path = os.path.join(package_dir, 'templates')
62        super().__init__(template_path)
63
64    @classmethod
65    def render_from_file(cls, filename: str, context: Dict) -> str:
66        return FileRenderer.render_from_file(filename, context)
67
68
69class LaTeXRenderer(SphinxRenderer):
70    def __init__(self, template_path: str = None, latex_engine: str = None) -> None:
71        if template_path is None:
72            template_path = os.path.join(package_dir, 'templates', 'latex')
73        super().__init__(template_path)
74
75        # use texescape as escape filter
76        escape = partial(texescape.escape, latex_engine=latex_engine)
77        self.env.filters['e'] = escape
78        self.env.filters['escape'] = escape
79        self.env.filters['eabbr'] = texescape.escape_abbr
80
81        # use JSP/eRuby like tagging instead because curly bracket; the default
82        # tagging of jinja2 is not good for LaTeX sources.
83        self.env.variable_start_string = '<%='
84        self.env.variable_end_string = '%>'
85        self.env.block_start_string = '<%'
86        self.env.block_end_string = '%>'
87        self.env.comment_start_string = '<#'
88        self.env.comment_end_string = '#>'
89
90
91class ReSTRenderer(SphinxRenderer):
92    def __init__(self, template_path: Union[str, List[str]] = None, language: str = None) -> None:  # NOQA
93        super().__init__(template_path)
94
95        # add language to environment
96        self.env.extend(language=language)
97
98        # use texescape as escape filter
99        self.env.filters['e'] = rst.escape
100        self.env.filters['escape'] = rst.escape
101        self.env.filters['heading'] = rst.heading
102
103
104class SphinxTemplateLoader(BaseLoader):
105    """A loader supporting template inheritance"""
106
107    def __init__(self, confdir: str, templates_paths: List[str],
108                 system_templates_paths: List[str]) -> None:
109        self.loaders = []
110        self.sysloaders = []
111
112        for templates_path in templates_paths:
113            loader = SphinxFileSystemLoader(path.join(confdir, templates_path))
114            self.loaders.append(loader)
115
116        for templates_path in system_templates_paths:
117            loader = SphinxFileSystemLoader(templates_path)
118            self.loaders.append(loader)
119            self.sysloaders.append(loader)
120
121    def get_source(self, environment: Environment, template: str) -> Tuple[str, str, Callable]:
122        if template.startswith('!'):
123            # search a template from ``system_templates_paths``
124            loaders = self.sysloaders
125            template = template[1:]
126        else:
127            loaders = self.loaders
128
129        for loader in loaders:
130            try:
131                return loader.get_source(environment, template)
132            except TemplateNotFound:
133                pass
134        raise TemplateNotFound(template)
135