comparison libervia/pages/_browser/template.py @ 1306:c07112ef01cd

browser (template): adapted filters/global/extensions to manage SàT templates: `template` module has been update so most SàT template can be run from browser: - `profile` and `csrf_token` are set as globals - an implementation of `xmlattr` filter has been added - `date_fmt` filter has been implemented using `moment.js` - i18n method `_` has been added to globals, and `{% trans %}` statement has been implemented using an extension. For now they are not actually translating but just returning the unmodified string. - new `get_args` helper method to handle `nunjucks` convention for arguments. - fixed `get_elt` to only return the first child element (avoiding any text child) + added a defaut value for `context`
author Goffi <goffi@goffi.org>
date Thu, 16 Jul 2020 09:08:50 +0200
parents 8aba2a2078ca
children 3411ec23c389
comparison
equal deleted inserted replaced
1305:db9ea167c409 1306:c07112ef01cd
13 'trimBlocks': True, 13 'trimBlocks': True,
14 'lstripBlocks': True, 14 'lstripBlocks': True,
15 }) 15 })
16 16
17 nunjucks.installJinjaCompat() 17 nunjucks.installJinjaCompat()
18 env.addGlobal("profile", window.profile)
19 env.addGlobal("csrf_token", window.csrf_token)
20 # FIXME: integrate gettext or equivalent here
21 env.addGlobal("_", lambda txt: txt)
18 22
19 23
20 class Indexer: 24 class Indexer:
21 """Index global to a page""" 25 """Index global to a page"""
22 26
36 40
37 gidx = Indexer() 41 gidx = Indexer()
38 # suffix use to avoid collision with IDs generated in static page 42 # suffix use to avoid collision with IDs generated in static page
39 SCRIPT_SUFF = "__script__" 43 SCRIPT_SUFF = "__script__"
40 44
45 def escape_html(txt):
46 return (
47 txt
48 .replace('&', '&amp;')
49 .replace('<', '&lt;')
50 .replace('>', '&gt;')
51 .replace('"', '&quot;')
52 )
53
54
55 def get_args(n_args, *sig_args, **sig_kwargs):
56 """Retrieve function args when they are transmitted using nunjucks convention
57
58 cf. https://mozilla.github.io/nunjucks/templating.html#keyword-arguments
59 @param n_args: argument from nunjucks call
60 @param sig_args: expected positional arguments
61 @param sig_kwargs: expected keyword arguments
62 @return: all expected arguments, with default value if not specified in nunjucks
63 """
64 # nunjucks set kwargs in last argument
65 given_args = list(n_args)
66 try:
67 given_kwargs = given_args.pop().to_dict()
68 except (AttributeError, IndexError):
69 # we don't have a dict as last argument
70 # that happens when there is no keyword argument
71 given_args = list(n_args)
72 given_kwargs = {}
73 ret = given_args[:len(sig_args)]
74 # we check if we have remaining positional arguments
75 # in which case they may be specified in keyword arguments
76 for name in sig_args[len(given_args):]:
77 try:
78 value = given_kwargs.pop(name)
79 except KeyError:
80 raise ValueError(f"missing positional arguments {name!r}")
81 ret.append(value)
82
83 extra_pos_args = given_args[len(sig_args):]
84 # and now the keyword arguments
85 for name, default in sig_kwargs.items():
86 if extra_pos_args:
87 # kw args has been specified with a positional argument
88 ret.append(extra_pos_args.pop(0))
89 continue
90 value = given_kwargs.get(name, default)
91 ret.append(value)
92
93 return ret
94
41 95
42 def _next_gidx(value): 96 def _next_gidx(value):
43 """Use next current global index as suffix""" 97 """Use next current global index as suffix"""
44 next_ = gidx.next(value) 98 next_ = gidx.next(value)
45 return f"{value}{SCRIPT_SUFF}" if next_ == 0 else f"{value}_{SCRIPT_SUFF}{next_}" 99 return f"{value}{SCRIPT_SUFF}" if next_ == 0 else f"{value}_{SCRIPT_SUFF}{next_}"
53 return f"{value}{SCRIPT_SUFF}" if not current else f"{value}_{SCRIPT_SUFF}{current}" 107 return f"{value}{SCRIPT_SUFF}" if not current else f"{value}_{SCRIPT_SUFF}{current}"
54 108
55 env.addFilter("cur_gidx", _cur_gidx) 109 env.addFilter("cur_gidx", _cur_gidx)
56 110
57 111
112 def _xmlattr(d, autospace=True):
113 if not d:
114 return
115 d = d.to_dict()
116 ret = [''] if autospace else []
117 for key, value in d.items():
118 if value is not None:
119 ret.append(f'{escape_html(key)}="{escape_html(str(value))}"')
120
121 return safe(' '.join(ret))
122
123 env.addFilter("xmlattr", _xmlattr)
124
125
58 def _tojson(value): 126 def _tojson(value):
59 return safe( 127 return safe(escape_html(window.JSON.stringify(value)))
60 window.JSON.stringify(value)
61 .replace('&', '&amp;')
62 .replace('<', '&lt;')
63 .replace('>', '&gt;')
64 .replace('"', '&quot;')
65 )
66 128
67 env.addFilter("tojson", _tojson) 129 env.addFilter("tojson", _tojson)
68 130
69 131
70 def _icon_use(name, cls=""): 132 def _icon_use(name, cls=""):
78 ) 140 )
79 141
80 env.addGlobal("icon", _icon_use) 142 env.addGlobal("icon", _icon_use)
81 143
82 144
145 def _date_fmt(
146 timestamp, *args
147 ):
148 """Date formatting
149
150 cf. sat.tools.common.date_utils for arguments details
151 """
152 fmt, date_only, auto_limit, auto_old_fmt, auto_new_fmt = get_args(
153 args, fmt="short", date_only=False, auto_limit=7, auto_old_fmt="short",
154 auto_new_fmt="relative",
155 )
156 from js_modules.moment import moment
157 date = moment.unix(timestamp)
158
159 if fmt == "auto_day":
160 fmt, auto_limit, auto_old_fmt, auto_new_fmt = "auto", 0, "short", "HH:mm"
161 if fmt == "auto":
162 limit = moment().startOf('day').subtract(auto_limit, 'days')
163 m_fmt = auto_old_fmt if date < limit else auto_new_fmt
164
165 if fmt == "short":
166 m_fmt = "DD/MM/YY" if date_only else "DD/MM/YY HH:mm"
167 elif fmt == "medium":
168 m_fmt = "ll" if date_only else "lll"
169 elif fmt == "long":
170 m_fmt = "LL" if date_only else "LLL"
171 elif fmt == "full":
172 m_fmt = "dddd, LL" if date_only else "LLLL"
173 elif fmt == "relative":
174 return date.fromNow()
175 elif fmt == "iso":
176 if date_only:
177 m_fmt == "YYYY-MM-DD"
178 else:
179 return date.toISOString()
180 else:
181 raise NotImplementedError("free format is not implemented yet")
182
183 return date.format(m_fmt)
184
185 env.addFilter("date_fmt", _date_fmt)
186
187
188 class I18nExtension:
189 """Extension to handle the {% trans %}{% endtrans %} statement"""
190 # FIXME: for now there is no translation, this extension only returns the string
191 # unmodified
192 tags = ['trans']
193
194 def parse(self, parser, nodes, lexer):
195 tok = parser.nextToken()
196 args = parser.parseSignature(None, True)
197 parser.advanceAfterBlockEnd(tok.value)
198 body = parser.parseUntilBlocks('endtrans')
199 parser.advanceAfterBlockEnd()
200 return nodes.CallExtension.new(self._js_ext, 'run', args, [body])
201
202 def run(self, context, *args):
203 body = args[-1]
204 return body()
205
206 @classmethod
207 def install(cls, env):
208 ext = cls()
209 ext_dict = {
210 "tags": ext.tags,
211 "parse": ext.parse,
212 "run": ext.run
213 }
214 ext._js_ext = javascript.pyobj2jsobj(ext_dict)
215 env.addExtension(cls.__name__, ext._js_ext)
216
217 I18nExtension.install(env)
218
219
83 class Template: 220 class Template:
84 221
85 def __init__(self, tpl_name): 222 def __init__(self, tpl_name):
86 self._tpl = env.getTemplate(tpl_name, True) 223 self._tpl = env.getTemplate(tpl_name, True)
87 224
88 def render(self, context): 225 def render(self, context):
89 return self._tpl.render(context) 226 return self._tpl.render(context)
90 227
91 def get_elt(self, context): 228 def get_elt(self, context=None):
229 if context is None:
230 context = {}
92 raw_html = self.render(context) 231 raw_html = self.render(context)
93 template_elt = document.createElement('template') 232 template_elt = document.createElement('template')
94 template_elt.innerHTML = raw_html 233 template_elt.innerHTML = raw_html
95 return template_elt.content.firstChild 234 return template_elt.content.firstElementChild