summaryrefslogtreecommitdiffstats
path: root/sphinx/util/template.py
blob: a16a7a127fb4bca339059492d673645cabdc3736 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
"""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
        self.env.install_gettext_translations(get_translator())

    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, 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, 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)