beef331 / website

Code for the official Nim programming language website
https://nim-lang.org
19 stars 1 forks source link

Nimja #44

Closed enthus1ast closed 2 years ago

enthus1ast commented 2 years ago

Name: Nimja

Author: David Krause (enthus1ast)

Nimja

Nimja is a compiled, type safe and fast templating engine written in Nim. It looks like python's jinja2 or PHP's twig, but in contrast to them, it is fully compiled and type safe, since it compiles down to Nim. All the heavy lifting is done on compile time, and the resulting binary contains idiomatic Nim that is very fast on runtime.

# nimble install nimja
import nimja
proc renderStuff(): string =
  compileTemplateStr("""
    {% extends partials/_master.nwt%}
    {% block content %}

      <h1>Random links</h1>

      {% const links = [
        (title: "google", target: "https://google.de"),
        (title: "nim", target: "https://nim-lang.org")]
      %}

      {% for (ii, item) in links.pairs() %}
        {{ii}} <a href="{{item.target}}">This is a link to: {{item.title}}</a><br>
      {% endfor %}

      <h1>Members</h1>
      {% for (idx, user) in users.pairs %}
        <a href="/users/{{idx}}">{% importnwt "./partials/_user.nwt" %}</a><br>
      {% endfor %}

    {% endblock %}
  """)

Since Nimja transforms the templates to Nim on compile time, most Nim code is valid in the templates.

For example:

import times

iterator someProc(someVar: int): string =
  for idx in 0 .. someVar:
    yield "foo"

proc renderStuff(someVar: int): string =
  compileTemplateStr("""
    the current date is {{ now() }}
    <ul>
      {% for str in someProc(someVar): %}
        <li>{{str}}</li>
      {% endfor %}
    </ul>
  """)

echo renderStuff(10)

Nimja not only supports all the basics of a template engine:

if, for, while etc.

but also advanced features like:

extend or white space control.

Extend

Extend in particular is very useful for building websites. It basically allows a child template to choose its outer template.

This way you could just render a detail page, and have it automatically render its surrounding boilerplate.

For example:

master.html

<html>
  <head><title>MyPage - {% block title %}{% endblock %}</title></head>
  <body>
    <h1>{{self.title}}</h1>
    <div class="content">
      {% block content %}{% endblock %}
    </div>
  </body>
</html>

detail.html

{% extends master.html %}
{% block title %}Detail Page{% endblock %}
{% block content %}I am content{% endblock %}

When the detail page is rendered in your application, also the master.html is rendered implicitly:

import nimja
import os # for `/`
proc render(): string =
  compileTemplateFile(getScriptDir() / "detail.html")
echo render()

multiple level of extend works as you would expect it.

Imports

Another useful feature of Nimja are the imports. This allows to import a template into another (also multiple times). So you can create small building blocks to use all over your website.

user.html

<div class="user">
  User: {{user.name}} {{user.lastname}} age: {{user.age}}
</div>

detail.html

{% extends "master.html" %}
{# we define the users here for illustration, but they can come from db etc.. #}
{% let users: seq[tuple[name, lastname: string, age: int]] = @[
  ("David", "Krause", 33),
  ("Katja", "Kopylevic", 32)
] %}
<div class="users">
  {% for user in users %}
    {% importnwt getScriptDir() / "user.html" %}
  {% endfor %}
</div>

Very Fast

Nimja is not explicitly optimized for performance, but is has some small optimization passes, for example it tries to minimize the number of string concatenations by combining them. Also the way Nimja works (mostly on compile time), makes it very fast.

Ajusa has made a simple template engine benchmark, and I'm very proud that Nimja is that fast:

# https://github.com/enthus1ast/dekao/blob/master/bench.nim
# nim c --gc:arc -d:release -d:danger -d:lto --opt:speed -r bench.nim
name ............................... min time  avg time  std dv   runs
dekao .............................. 0.105 ms  0.117 ms  ±0.013  x1000
karax .............................. 0.126 ms  0.132 ms  ±0.008  x1000
htmlgen ............................ 0.021 ms  0.023 ms  ±0.004  x1000
nimja .............................. 0.016 ms  0.017 ms  ±0.001  x1000 <--
nimja iterator ..................... 0.008 ms  0.009 ms  ±0.001  x1000 <--
scf ................................ 0.023 ms  0.024 ms  ±0.003  x1000
nim-mustache ....................... 0.745 ms  0.790 ms  ±0.056  x1000

The meaningfulness of such benchmarks are of course worth discussing, but it shows that you must not sacrifice runtime performance for the joy and ease of use (thanks to Nim's macros).

How?

Nimja works by compiling its templates to Nim code:

for example this:

proc foo(ss: string, ii: int): string =
  compileTemplateStr(
    """example{% if ii == 1%}{{ss}}{%endif%}{% var myvar = 1 %}{% myvar.inc %}"""
  )

is transformed to this:

proc foo(ss: string; ii: int): string =
  result &= "example"
  if ii == 1:
    result &= ss
  var myvar = 1
  inc(myvar, 1)

Since result &= "mystring is generated, you can choose any return type, that implements a &= proc. This makes the process quite flexible. So instead of a string, you could also return a Rope or similar.

Nimja can also generate iterator bodies. So instead of string concatenation, it can also yield.

iterator foo(ss: string, ii: int): string =
  compileTemplateStr(
    """example{% if ii == 1%}{{ss}}{%endif%}{% var myvar = 1 %}{% myvar.inc %}""",
    iter = true
  )

If the web server is able to send data in chunks, you could save some memory, since only small chunks must be stored. In the benchmark, this approach is twice as fast.

Hot Code Reloading

Nimja is a compiled template engine, this means you must recompile your application for every change you do in the templates. This could be quite annoying and time consuming.

To streamline the experience a little bit, Nimja ships with Hot Code Reloading utilities.

This means it can compile templates to a dynamic library and recompile and reload this library on template change.

For details have a look at the Hot Code Reloading section of the readme.

"Fileless"

The templates are compiled to binary and stored in the executable. This means that the templates must not be shipped in cleartext.

In addition Nimjautils has utility functions that helps store small assets in the executable.

eg.: includeRawStatic and includeStaticAsDataurl

this could be used for single executable "electron like" gui libraries.

Looks familiar

I've build Nimja to port jinja2 and twig websites to Nim. This means it tries to be as similar to both template languages as possible, without loosing it's "Nim'ness"

Porting often means to just change a few lines of code, or add another "filter" you've used in one of the other engines.

Give Nimja a try it's fun!

https://github.com/enthus1ast/nimja

beef331 commented 2 years ago

Can you reduce it a bit? I'm sure Miran will complain :stuck_out_tongue:

enthus1ast commented 2 years ago

Yeah, the template examples bloat this post a little :/
@narimiran would you complain?

narimiran commented 2 years ago

@narimiran would you complain?

Seeing the length of this, I think it needs some changing. But not in the direction you expect :)

Do not reduce it. Show us even more details, and then — this is not anymore an entry for This Month with Nim, it becomes a full guest-post on its own.

enthus1ast commented 2 years ago

@narimiran this would be awesome! Should i change it here inplace or put it somewhere else?

narimiran commented 2 years ago

Should i change it here inplace or put it somewhere else?

If you want, you can change it here and ping me once it is ready, or you can immediately do what I would then do in the next step: Fork nim-lang/website and then create a PR there.

Here are the two examples of recent guest posts:

enthus1ast commented 2 years ago

@beef331 im working on the guest post. Wanted to do some changes first.