Mercurial > libervia-web
diff 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 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/libervia/web/pages/_browser/template.py Fri Jun 02 16:49:28 2023 +0200 @@ -0,0 +1,235 @@ +"""Integrate templating system using nunjucks""" + +from js_modules.nunjucks import nunjucks +from browser import window, document +import javascript + + +safe = nunjucks.runtime.SafeString.new +env = nunjucks.configure( + window.templates_root_url, + { + 'autoescape': True, + 'trimBlocks': True, + 'lstripBlocks': True, + 'web': {'use_cache': True}, + }) + +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: + """Index global to a page""" + + def __init__(self): + self._indexes = {} + + def next(self, value): + if value not in self._indexes: + self._indexes[value] = 0 + return 0 + self._indexes[value] += 1 + return self._indexes[value] + + def current(self, value): + return self._indexes.get(value) + + +gidx = Indexer() +# 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""" + next_ = gidx.next(value) + return f"{value}{SCRIPT_SUFF}" if next_ == 0 else f"{value}_{SCRIPT_SUFF}{next_}" + +env.addFilter("next_gidx", _next_gidx) + + +def _cur_gidx(value): + """Use current current global index as suffix""" + current = gidx.current(value) + return f"{value}{SCRIPT_SUFF}" if not current else f"{value}_{SCRIPT_SUFF}{current}" + +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(escape_html(window.JSON.stringify(value))) + +env.addFilter("tojson", _tojson) + + +def _icon_use(name, cls=""): + kwargs = cls.to_dict() + cls = kwargs.get('cls') + return safe( + '<svg class="svg-icon{cls}" xmlns="http://www.w3.org/2000/svg" ' + 'viewBox="0 0 100 100">\n' + ' <use href="#{name}"/>' + '</svg>\n'.format(name=name, cls=(" " + cls) if cls else "") + ) + +env.addGlobal("icon", _icon_use) + + +def _date_fmt( + timestamp, *args +): + """Date formatting + + cf. libervia.backend.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): + self._tpl = env.getTemplate(tpl_name, True) + + def render(self, context): + return self._tpl.render(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.firstElementChild