Mercurial > libervia-web
comparison libervia/web/pages/_browser/template.py @ 1518:eb00d593801d
refactoring: rename `libervia` to `libervia.web` + update imports following backend changes
author | Goffi <goffi@goffi.org> |
---|---|
date | Fri, 02 Jun 2023 16:49:28 +0200 |
parents | libervia/pages/_browser/template.py@106bae41f5c8 |
children | de7e468e2d44 |
comparison
equal
deleted
inserted
replaced
1517:b8ed9726525b | 1518:eb00d593801d |
---|---|
1 """Integrate templating system using nunjucks""" | |
2 | |
3 from js_modules.nunjucks import nunjucks | |
4 from browser import window, document | |
5 import javascript | |
6 | |
7 | |
8 safe = nunjucks.runtime.SafeString.new | |
9 env = nunjucks.configure( | |
10 window.templates_root_url, | |
11 { | |
12 'autoescape': True, | |
13 'trimBlocks': True, | |
14 'lstripBlocks': True, | |
15 'web': {'use_cache': True}, | |
16 }) | |
17 | |
18 nunjucks.installJinjaCompat() | |
19 env.addGlobal("profile", window.profile) | |
20 env.addGlobal("csrf_token", window.csrf_token) | |
21 # FIXME: integrate gettext or equivalent here | |
22 env.addGlobal("_", lambda txt: txt) | |
23 | |
24 | |
25 class Indexer: | |
26 """Index global to a page""" | |
27 | |
28 def __init__(self): | |
29 self._indexes = {} | |
30 | |
31 def next(self, value): | |
32 if value not in self._indexes: | |
33 self._indexes[value] = 0 | |
34 return 0 | |
35 self._indexes[value] += 1 | |
36 return self._indexes[value] | |
37 | |
38 def current(self, value): | |
39 return self._indexes.get(value) | |
40 | |
41 | |
42 gidx = Indexer() | |
43 # suffix use to avoid collision with IDs generated in static page | |
44 SCRIPT_SUFF = "__script__" | |
45 | |
46 def escape_html(txt): | |
47 return ( | |
48 txt | |
49 .replace('&', '&') | |
50 .replace('<', '<') | |
51 .replace('>', '>') | |
52 .replace('"', '"') | |
53 ) | |
54 | |
55 | |
56 def get_args(n_args, *sig_args, **sig_kwargs): | |
57 """Retrieve function args when they are transmitted using nunjucks convention | |
58 | |
59 cf. https://mozilla.github.io/nunjucks/templating.html#keyword-arguments | |
60 @param n_args: argument from nunjucks call | |
61 @param sig_args: expected positional arguments | |
62 @param sig_kwargs: expected keyword arguments | |
63 @return: all expected arguments, with default value if not specified in nunjucks | |
64 """ | |
65 # nunjucks set kwargs in last argument | |
66 given_args = list(n_args) | |
67 try: | |
68 given_kwargs = given_args.pop().to_dict() | |
69 except (AttributeError, IndexError): | |
70 # we don't have a dict as last argument | |
71 # that happens when there is no keyword argument | |
72 given_args = list(n_args) | |
73 given_kwargs = {} | |
74 ret = given_args[:len(sig_args)] | |
75 # we check if we have remaining positional arguments | |
76 # in which case they may be specified in keyword arguments | |
77 for name in sig_args[len(given_args):]: | |
78 try: | |
79 value = given_kwargs.pop(name) | |
80 except KeyError: | |
81 raise ValueError(f"missing positional arguments {name!r}") | |
82 ret.append(value) | |
83 | |
84 extra_pos_args = given_args[len(sig_args):] | |
85 # and now the keyword arguments | |
86 for name, default in sig_kwargs.items(): | |
87 if extra_pos_args: | |
88 # kw args has been specified with a positional argument | |
89 ret.append(extra_pos_args.pop(0)) | |
90 continue | |
91 value = given_kwargs.get(name, default) | |
92 ret.append(value) | |
93 | |
94 return ret | |
95 | |
96 | |
97 def _next_gidx(value): | |
98 """Use next current global index as suffix""" | |
99 next_ = gidx.next(value) | |
100 return f"{value}{SCRIPT_SUFF}" if next_ == 0 else f"{value}_{SCRIPT_SUFF}{next_}" | |
101 | |
102 env.addFilter("next_gidx", _next_gidx) | |
103 | |
104 | |
105 def _cur_gidx(value): | |
106 """Use current current global index as suffix""" | |
107 current = gidx.current(value) | |
108 return f"{value}{SCRIPT_SUFF}" if not current else f"{value}_{SCRIPT_SUFF}{current}" | |
109 | |
110 env.addFilter("cur_gidx", _cur_gidx) | |
111 | |
112 | |
113 def _xmlattr(d, autospace=True): | |
114 if not d: | |
115 return | |
116 d = d.to_dict() | |
117 ret = [''] if autospace else [] | |
118 for key, value in d.items(): | |
119 if value is not None: | |
120 ret.append(f'{escape_html(key)}="{escape_html(str(value))}"') | |
121 | |
122 return safe(' '.join(ret)) | |
123 | |
124 env.addFilter("xmlattr", _xmlattr) | |
125 | |
126 | |
127 def _tojson(value): | |
128 return safe(escape_html(window.JSON.stringify(value))) | |
129 | |
130 env.addFilter("tojson", _tojson) | |
131 | |
132 | |
133 def _icon_use(name, cls=""): | |
134 kwargs = cls.to_dict() | |
135 cls = kwargs.get('cls') | |
136 return safe( | |
137 '<svg class="svg-icon{cls}" xmlns="http://www.w3.org/2000/svg" ' | |
138 'viewBox="0 0 100 100">\n' | |
139 ' <use href="#{name}"/>' | |
140 '</svg>\n'.format(name=name, cls=(" " + cls) if cls else "") | |
141 ) | |
142 | |
143 env.addGlobal("icon", _icon_use) | |
144 | |
145 | |
146 def _date_fmt( | |
147 timestamp, *args | |
148 ): | |
149 """Date formatting | |
150 | |
151 cf. libervia.backend.tools.common.date_utils for arguments details | |
152 """ | |
153 fmt, date_only, auto_limit, auto_old_fmt, auto_new_fmt = get_args( | |
154 args, fmt="short", date_only=False, auto_limit=7, auto_old_fmt="short", | |
155 auto_new_fmt="relative", | |
156 ) | |
157 from js_modules.moment import moment | |
158 date = moment.unix(timestamp) | |
159 | |
160 if fmt == "auto_day": | |
161 fmt, auto_limit, auto_old_fmt, auto_new_fmt = "auto", 0, "short", "HH:mm" | |
162 if fmt == "auto": | |
163 limit = moment().startOf('day').subtract(auto_limit, 'days') | |
164 m_fmt = auto_old_fmt if date < limit else auto_new_fmt | |
165 | |
166 if fmt == "short": | |
167 m_fmt = "DD/MM/YY" if date_only else "DD/MM/YY HH:mm" | |
168 elif fmt == "medium": | |
169 m_fmt = "ll" if date_only else "lll" | |
170 elif fmt == "long": | |
171 m_fmt = "LL" if date_only else "LLL" | |
172 elif fmt == "full": | |
173 m_fmt = "dddd, LL" if date_only else "LLLL" | |
174 elif fmt == "relative": | |
175 return date.fromNow() | |
176 elif fmt == "iso": | |
177 if date_only: | |
178 m_fmt == "YYYY-MM-DD" | |
179 else: | |
180 return date.toISOString() | |
181 else: | |
182 raise NotImplementedError("free format is not implemented yet") | |
183 | |
184 return date.format(m_fmt) | |
185 | |
186 env.addFilter("date_fmt", _date_fmt) | |
187 | |
188 | |
189 class I18nExtension: | |
190 """Extension to handle the {% trans %}{% endtrans %} statement""" | |
191 # FIXME: for now there is no translation, this extension only returns the string | |
192 # unmodified | |
193 tags = ['trans'] | |
194 | |
195 def parse(self, parser, nodes, lexer): | |
196 tok = parser.nextToken() | |
197 args = parser.parseSignature(None, True) | |
198 parser.advanceAfterBlockEnd(tok.value) | |
199 body = parser.parseUntilBlocks('endtrans') | |
200 parser.advanceAfterBlockEnd() | |
201 return nodes.CallExtension.new(self._js_ext, 'run', args, [body]) | |
202 | |
203 def run(self, context, *args): | |
204 body = args[-1] | |
205 return body() | |
206 | |
207 @classmethod | |
208 def install(cls, env): | |
209 ext = cls() | |
210 ext_dict = { | |
211 "tags": ext.tags, | |
212 "parse": ext.parse, | |
213 "run": ext.run | |
214 } | |
215 ext._js_ext = javascript.pyobj2jsobj(ext_dict) | |
216 env.addExtension(cls.__name__, ext._js_ext) | |
217 | |
218 I18nExtension.install(env) | |
219 | |
220 | |
221 class Template: | |
222 | |
223 def __init__(self, tpl_name): | |
224 self._tpl = env.getTemplate(tpl_name, True) | |
225 | |
226 def render(self, context): | |
227 return self._tpl.render(context) | |
228 | |
229 def get_elt(self, context=None): | |
230 if context is None: | |
231 context = {} | |
232 raw_html = self.render(context) | |
233 template_elt = document.createElement('template') | |
234 template_elt.innerHTML = raw_html | |
235 return template_elt.content.firstElementChild |