Mercurial > libervia-web
view libervia/web/pages/_browser/template.py @ 1618:5d9889f14012 default tip @
server: start major redesign
- Add icons to menu items
- Switch menu items representation from tuple to dictionary for future extensibility:
- Include icon information
- Prepare for additional data
- Remove "login" from main menu, add login page URL to template data, as it is now a separate right-aligned item
author | Goffi <goffi@goffi.org> |
---|---|
date | Sat, 26 Oct 2024 23:07:01 +0200 |
parents | 9865013da86c |
children |
line wrap: on
line source
"""Integrate templating system using nunjucks""" from js_modules.nunjucks import nunjucks from browser import window, document import javascript import jid 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 used 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 _bare_jid(full_jid: str|jid.JID) -> str: """Return the bare JID""" return str(jid.JID(str(full_jid)).bare) env.addFilter("bare_jid", _bare_jid) 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 _media_type_main(value: str|None) -> str|None: """Return main type of a media type""" if not value: return None return value.partition("/")[0] env.addFilter("media_type_main", _media_type_main) def _media_type_sub(value: str|None) -> str|None: """Return main type of a media type""" if not value: return None return value.partition("/")[1] env.addFilter("media_type_sub", _media_type_sub) def _icon_use(name, cls=""): if 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') fmt = auto_old_fmt if date < limit else auto_new_fmt format_mapping = { "short": "DD/MM/YY" if date_only else "DD/MM/YY HH:mm", "medium": "DD MMM YYYY" if date_only else "DD MMM YYYY HH:mm", "long": "D MMMM YYYY" if date_only else "D MMMM YYYY HH:mm", "full": "dddd, D MMMM YYYY" if date_only else "dddd, D MMMM YYYY HH:mm", "relative": date.fromNow(), "iso": date.toISOString() if not date_only else "YYYY-MM-DD" } # if `fmt` doesn't match any mapping, it's a free format m_fmt = format_mapping.get(fmt, fmt) return m_fmt if fmt in ["relative", "iso"] else 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