StrictDoc Documentation
strictdoc/export/html/html_templates.py
Source file coverage
Path:
strictdoc/export/html/html_templates.py
Lines:
155
Non-empty lines:
131
Non-empty lines covered with requirements:
131 / 131 (100.0%)
Functions:
16
Functions covered by requirements:
16 / 16 (100.0%)
1
import datetime
2
import glob
3
import hashlib
4
import os.path
5
import shutil
6
from pathlib import Path
7
from typing import Any, List, Optional
8
 
9
from jinja2 import (
10
    Environment,
11
    FileSystemLoader,
12
    ModuleLoader,
13
    StrictUndefined,
14
    Template,
15
)
16
from markupsafe import Markup
17
 
18
from strictdoc import environment
19
from strictdoc.core.project_config import ProjectConfig
20
from strictdoc.export.html.jinja.assert_extension import AssertExtension
21
from strictdoc.helpers.file_modification_time import get_file_modification_time
22
from strictdoc.helpers.timing import measure_performance
23
 
24
 
25
class JinjaEnvironment:
26
    environment: Environment
27
 
28
    def __init__(self, environment: Environment):
29
        self.environment = environment
30
 
31
    def get_template(self, *args: Any, **kwargs: Any) -> Template:
32
        return self.environment.get_template(*args, **kwargs)
33
 
34
    def render_template_as_markup(
35
        self, template: str, *args: Any, **kwargs: Any
36
    ) -> Markup:
37
        return Markup(
38
            self.environment.get_template(template).render(*args, **kwargs)
39
        )
40
 
41
 
42
class HTMLTemplates:
43
    @staticmethod
44
    def create(
45
        project_config: ProjectConfig,
46
        enable_caching: bool,
47
        strictdoc_last_update: datetime.datetime,
48
    ) -> "HTMLTemplates":
49
        assert isinstance(strictdoc_last_update, datetime.datetime)
50
        if enable_caching:
51
            cacheable_templates = CompiledHTMLTemplates(project_config)
52
            cacheable_templates.reset_jinja_environment_if_outdated(
53
                strictdoc_last_update
54
            )
55
            cacheable_templates.compile_jinja_templates()
56
            return CompiledHTMLTemplates(project_config)
57
 
58
        return NormalHTMLTemplates()
59
 
60
    def jinja_environment(self) -> JinjaEnvironment:
61
        raise NotImplementedError
62
 
63
 
64
class NormalHTMLTemplates(HTMLTemplates):
65
    def __init__(self) -> None:
66
        self._jinja_environment: JinjaEnvironment = JinjaEnvironment(
67
            Environment(
68
                loader=FileSystemLoader(
69
                    environment.get_path_to_html_templates()
70
                ),
71
                undefined=StrictUndefined,
72
                extensions=[AssertExtension],
73
                autoescape=True,
74
            )
75
        )
76
 
77
    def jinja_environment(self) -> JinjaEnvironment:
78
        return self._jinja_environment
79
 
80
 
81
class CompiledHTMLTemplates(HTMLTemplates):
82
    def __init__(self, project_config: ProjectConfig):
83
        path_to_output_dir_hash = hashlib.md5(
84
            project_config.output_dir.encode("utf-8")
85
        ).hexdigest()
86
        self.path_to_jinja_cache_bucket_dir = os.path.join(
87
            project_config.get_path_to_cache_dir(),
88
            "jinja",
89
            path_to_output_dir_hash,
90
        )
91
        self._jinja_environment: Optional[JinjaEnvironment] = None
92
 
93
    def compile_jinja_templates(self) -> None:
94
        if os.path.isdir(self.path_to_jinja_cache_bucket_dir):
95
            return
96
        jinja_environment = Environment(
97
            loader=FileSystemLoader(environment.get_path_to_html_templates()),
98
            undefined=StrictUndefined,
99
            extensions=[AssertExtension],
100
            autoescape=True,
101
        )
102
        # TODO: Check if this line is still needed (might be some older workaround).
103
        jinja_environment.globals.update(isinstance=isinstance)
104
        with measure_performance("Compile Jinja templates"):
105
 
106
            def filter_function_(name: str) -> bool:
107
                # On macOS, the .DS_Store files make Jinja templates compiler
108
                # to crash.
109
                # https://github.com/strictdoc-project/strictdoc/issues/1266
110
                if name.endswith(".DS_Store"):
111
                    return False
112
                return True
113
 
114
            Path(self.path_to_jinja_cache_bucket_dir).mkdir(
115
                parents=True, exist_ok=True
116
            )
117
            jinja_environment.compile_templates(
118
                self.path_to_jinja_cache_bucket_dir,
119
                zip=None,
120
                filter_func=filter_function_,
121
                ignore_errors=False,
122
            )
123
 
124
    def jinja_environment(self) -> JinjaEnvironment:
125
        if self._jinja_environment is not None:
126
            return self._jinja_environment
127
        assert os.path.isdir(self.path_to_jinja_cache_bucket_dir)
128
        self._jinja_environment = JinjaEnvironment(
129
            Environment(
130
                loader=ModuleLoader(self.path_to_jinja_cache_bucket_dir),
131
                undefined=StrictUndefined,
132
                extensions=[AssertExtension],
133
                autoescape=True,
134
            )
135
        )
136
        return self._jinja_environment
137
 
138
    def reset_jinja_environment_if_outdated(
139
        self, strictdoc_last_update: datetime.datetime
140
    ) -> None:
141
        assert isinstance(strictdoc_last_update, datetime.datetime)
142
 
143
        if os.path.isdir(self.path_to_jinja_cache_bucket_dir):
144
            jinja_cache_files: List[str] = list(
145
                glob.iglob(
146
                    f"{self.path_to_jinja_cache_bucket_dir}/**/*.py",
147
                    recursive=True,
148
                )
149
            )
150
 
151
            jinja_cache_mtime = get_file_modification_time(jinja_cache_files[0])
152
 
153
            if strictdoc_last_update > jinja_cache_mtime:
154
                self._jinja_environment = None
155
                shutil.rmtree(self.path_to_jinja_cache_bucket_dir)