# HG changeset patch # User Goffi # Date 1594883330 -7200 # Node ID c07112ef01cd27bc21e3308e9ebf815862e09d41 # Parent db9ea167c409ce7a0f6bce0d8467ccf42a626b14 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` diff -r db9ea167c409 -r c07112ef01cd libervia/pages/_browser/template.py --- a/libervia/pages/_browser/template.py Thu Jul 16 09:08:50 2020 +0200 +++ b/libervia/pages/_browser/template.py Thu Jul 16 09:08:50 2020 +0200 @@ -15,6 +15,10 @@ }) nunjucks.installJinjaCompat() +env.addGlobal("profile", window.profile) +env.addGlobal("csrf_token", window.csrf_token) +# FIXME: integrate gettext or equivalent here +env.addGlobal("_", lambda txt: txt) class Indexer: @@ -38,6 +42,56 @@ # suffix use to avoid collision with IDs generated in static page SCRIPT_SUFF = "__script__" +def escape_html(txt): + return ( + txt + .replace('&', '&') + .replace('<', '<') + .replace('>', '>') + .replace('"', '"') + ) + + +def get_args(n_args, *sig_args, **sig_kwargs): + """Retrieve function args when they are transmitted using nunjucks convention + + cf. https://mozilla.github.io/nunjucks/templating.html#keyword-arguments + @param n_args: argument from nunjucks call + @param sig_args: expected positional arguments + @param sig_kwargs: expected keyword arguments + @return: all expected arguments, with default value if not specified in nunjucks + """ + # nunjucks set kwargs in last argument + given_args = list(n_args) + try: + given_kwargs = given_args.pop().to_dict() + except (AttributeError, IndexError): + # we don't have a dict as last argument + # that happens when there is no keyword argument + given_args = list(n_args) + given_kwargs = {} + ret = given_args[:len(sig_args)] + # we check if we have remaining positional arguments + # in which case they may be specified in keyword arguments + for name in sig_args[len(given_args):]: + try: + value = given_kwargs.pop(name) + except KeyError: + raise ValueError(f"missing positional arguments {name!r}") + ret.append(value) + + extra_pos_args = given_args[len(sig_args):] + # and now the keyword arguments + for name, default in sig_kwargs.items(): + if extra_pos_args: + # kw args has been specified with a positional argument + ret.append(extra_pos_args.pop(0)) + continue + value = given_kwargs.get(name, default) + ret.append(value) + + return ret + def _next_gidx(value): """Use next current global index as suffix""" @@ -55,14 +109,22 @@ env.addFilter("cur_gidx", _cur_gidx) +def _xmlattr(d, autospace=True): + if not d: + return + d = d.to_dict() + ret = [''] if autospace else [] + for key, value in d.items(): + if value is not None: + ret.append(f'{escape_html(key)}="{escape_html(str(value))}"') + + return safe(' '.join(ret)) + +env.addFilter("xmlattr", _xmlattr) + + def _tojson(value): - return safe( - window.JSON.stringify(value) - .replace('&', '&') - .replace('<', '<') - .replace('>', '>') - .replace('"', '"') - ) + return safe(escape_html(window.JSON.stringify(value))) env.addFilter("tojson", _tojson) @@ -80,6 +142,81 @@ env.addGlobal("icon", _icon_use) +def _date_fmt( + timestamp, *args +): + """Date formatting + + cf. sat.tools.common.date_utils for arguments details + """ + fmt, date_only, auto_limit, auto_old_fmt, auto_new_fmt = get_args( + args, fmt="short", date_only=False, auto_limit=7, auto_old_fmt="short", + auto_new_fmt="relative", + ) + from js_modules.moment import moment + date = moment.unix(timestamp) + + if fmt == "auto_day": + fmt, auto_limit, auto_old_fmt, auto_new_fmt = "auto", 0, "short", "HH:mm" + if fmt == "auto": + limit = moment().startOf('day').subtract(auto_limit, 'days') + m_fmt = auto_old_fmt if date < limit else auto_new_fmt + + if fmt == "short": + m_fmt = "DD/MM/YY" if date_only else "DD/MM/YY HH:mm" + elif fmt == "medium": + m_fmt = "ll" if date_only else "lll" + elif fmt == "long": + m_fmt = "LL" if date_only else "LLL" + elif fmt == "full": + m_fmt = "dddd, LL" if date_only else "LLLL" + elif fmt == "relative": + return date.fromNow() + elif fmt == "iso": + if date_only: + m_fmt == "YYYY-MM-DD" + else: + return date.toISOString() + else: + raise NotImplementedError("free format is not implemented yet") + + return date.format(m_fmt) + +env.addFilter("date_fmt", _date_fmt) + + +class I18nExtension: + """Extension to handle the {% trans %}{% endtrans %} statement""" + # FIXME: for now there is no translation, this extension only returns the string + # unmodified + tags = ['trans'] + + def parse(self, parser, nodes, lexer): + tok = parser.nextToken() + args = parser.parseSignature(None, True) + parser.advanceAfterBlockEnd(tok.value) + body = parser.parseUntilBlocks('endtrans') + parser.advanceAfterBlockEnd() + return nodes.CallExtension.new(self._js_ext, 'run', args, [body]) + + def run(self, context, *args): + body = args[-1] + return body() + + @classmethod + def install(cls, env): + ext = cls() + ext_dict = { + "tags": ext.tags, + "parse": ext.parse, + "run": ext.run + } + ext._js_ext = javascript.pyobj2jsobj(ext_dict) + env.addExtension(cls.__name__, ext._js_ext) + +I18nExtension.install(env) + + class Template: def __init__(self, tpl_name): @@ -88,8 +225,10 @@ def render(self, context): return self._tpl.render(context) - def get_elt(self, context): + def get_elt(self, context=None): + if context is None: + context = {} raw_html = self.render(context) template_elt = document.createElement('template') template_elt.innerHTML = raw_html - return template_elt.content.firstChild + return template_elt.content.firstElementChild