changeset 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 db9ea167c409
children 0a6698714557
files libervia/pages/_browser/template.py
diffstat 1 files changed, 148 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- 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('&', '&amp;')
+        .replace('<', '&lt;')
+        .replace('>', '&gt;')
+        .replace('"', '&quot;')
+    )
+
+
+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('&', '&amp;')
-        .replace('<',  '&lt;')
-        .replace('>', '&gt;')
-        .replace('"', '&quot;')
-    )
+    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