-
Notifications
You must be signed in to change notification settings - Fork 0
/
render.py
227 lines (193 loc) · 7.88 KB
/
render.py
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
import datetime
import functools
import pathlib
import re
import subprocess
from typing import Iterable, Any, Optional
import jinja2
def escape_tex(value, linebreaks=False):
"""
Escaping filter for the LaTeX Jinja2 environment.
:param value: The raw string to be escaped for usage in TeX files
:param linebreaks: If true, linebreaks are converted to TeX linebreaks ("\\")
:return: The escaped string
"""
if value is None:
return ""
latex_subs = [
(re.compile(r'\\'), r'\\textbackslash'),
(re.compile(r'([{}_#%&$])'), r'\\\1'),
(re.compile(r'~'), r'\~{}'),
(re.compile(r'\^'), r'\^{}'),
(re.compile(r'"'), r"''"),
]
if linebreaks:
latex_subs.append((re.compile(r'\n'), r'\\\\'))
res = str(value)
for pattern, replacement in latex_subs:
res = pattern.sub(replacement, res)
return res
def filter_inverse_chunks(value: Iterable[Any], n=2):
"""
A generator to be used as jinja filter that reverses chunks of n elements from the given iterator.
The last element will be repeated to fill the last chunk if neccessary.
:param value: Input iterator
:param n: Chunk size
"""
end = False
iterator = iter(value)
while not end:
chunk = []
for i in range(n):
try:
last = next(iterator)
except StopIteration:
end = True
if i == 0:
break
chunk.append(last)
for i in reversed(chunk):
yield i
def filter_date(value: Optional[datetime.date], format='%d.%m.%Y'):
"""
A filter to format date values.
:type value: datetime.date or None
:param format: a format string for the strftime function
"""
if value is None:
return ''
return value.strftime(format)
def filter_datetime(value: Optional[datetime.datetime], format='%d.%m.%Y~%H:%M', timezone=datetime.timezone.utc):
"""
A filter to format date values.
:type value: datetime.datetime or None
:param format: a format string for the strftime function
:param timezone: A timezone to convert the datetime object to before formatting
"""
if value is None:
return ''
return value.astimezone(timezone).strftime(format)
def find_asset(name: str, asset_dirs: Iterable[pathlib.Path]):
"""
Search the given asset directories for an asset with a given name and return its full path with '/' delimiters (to
be used in TeX).
:param name: The filename to search for. (May contain '/' to search in subdirectories.)
:param asset_dirs: List of asset directories to search for the given asset name
:type asset_dirs: [str]
:rtype: str
"""
for d in asset_dirs:
fullname = d / name
if fullname.is_file():
# make an explict conversion to posix paths, since this is expected by TeX
return fullname.as_posix()
return None
def get_latex_jinja_env(template_paths, asset_paths, timezone):
"""
Factory function to construct the Jinja2 Environment object. It sets the template loader, the Jinja variable-,
block- and comment delimiters, some additional options and the required filters and globals.
:param template_paths: A list of directories to be passed to the jinja2.FileSystemLoader to search for templates
:type template_paths: [str]
:param asset_paths: A list of directories to be searched for assets, using the `find_asset` template function
:type asset_paths: [str]
:param timezone: The timezone to show timestamps in
:type timezone: datetime.timezone
:return: The configured Jinja2 Environment
:rtype: jinja2.Environment
"""
latex_jinja2_env = jinja2.Environment(
loader=jinja2.FileSystemLoader(template_paths),
block_start_string='<<%',
block_end_string='%>>',
variable_start_string='<<<',
variable_end_string='>>>',
comment_start_string='<<#',
comment_end_string='#>>',
autoescape=False,
trim_blocks=True,
lstrip_blocks=False,
extensions=['jinja2.ext.do']
)
latex_jinja2_env.filters['e'] = escape_tex
latex_jinja2_env.filters['inverse_chunks'] = filter_inverse_chunks
latex_jinja2_env.filters['date'] = filter_date
latex_jinja2_env.filters['datetime'] = functools.partial(filter_datetime, timezone=timezone)
latex_jinja2_env.globals['now'] = datetime.datetime.now()
latex_jinja2_env.globals['find_asset'] = functools.partial(find_asset, asset_dirs=asset_paths)
return latex_jinja2_env
class ScheduleShutter:
"""
A small helper class to cancel scheduled function executions by wrapping the functions.
"""
def __init__(self):
self.shutdown = False
def wrap(self, fun):
@functools.wraps(fun)
def wrapped(*args, **kwargs):
if self.shutdown:
return
return fun(*args, **kwargs)
return wrapped
class RenderTask:
def __init__(self, template_name: str, job_name: str, template_args=None, double_tex=False):
self.template_name = template_name
self.job_name = job_name
self.template_args = template_args or {}
self.double_tex = double_tex
def render_template(task, output_dir: pathlib.Path, jinja_env, cleanup=True):
"""
Helper method to do the Jinja template rendering and LuaLaTeX execution.
:param task: A RenderJob tuple to define the job to be done. It contains the following fields:
template_name: filename of the Jinja template to render and compile
job_name: TeX jobname, defines filename of the output files
template_args: dict of arguments to be passed to the template
double_tex: if True, execute LuaLaTeX twice to allow building of links, tocs, longtables etc.
:type task: RenderTask
:param output_dir: Output directory. Absolute or relative path from working directory.
:type output_dir: str
:param jinja_env: The jinja Environment to use for template rendering
:param cleanup:
:type cleanup: bool
:return: True if rendering was successful
:rtype: bool
"""
# Get template
template = jinja_env.get_template(task.template_name)
# render template
outfile_name = task.job_name + '.tex'
with open(output_dir / outfile_name, 'w', encoding='utf-8') as outfile:
outfile.write(template.render(**task.template_args))
# Execute LuaLaTeX once
print('Compiling {}{} ...'.format(task.job_name, " once" if task.double_tex else ""))
process = subprocess.Popen(['lualatex', '--interaction=batchmode', outfile_name],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=output_dir)
process.wait()
rc = process.returncode
success = True
if rc != 0:
print("Compiling '{}' failed.{}".format(task.job_name, " (run 1)" if task.double_tex else ""))
success = False
# Execute LuaLaTeX second time
if success and task.double_tex:
print('Compiling {} a second time ...'.format(task.job_name))
process = subprocess.Popen(['lualatex', '--interaction=batchmode', outfile_name],
stdin=subprocess.DEVNULL,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
cwd=output_dir)
process.wait()
rc = process.returncode
if rc != 0:
print("Compiling '{}' failed. (run 2)")
success = False
# Clean up
if cleanup and success:
exp = re.compile(r'^{}\.(.+)$'.format(re.escape(task.job_name)))
for f in output_dir.iterdir():
match = re.match(exp, str(f.name))
if match and match.group(1) not in ('pdf',):
f.unlink()
return success