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('&', '&amp;')
50 .replace('<', '&lt;')
51 .replace('>', '&gt;')
52 .replace('"', '&quot;')
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