Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/enthus1ast/nimja

typed and compiled template engine inspired by jinja2, twig and onionhammer/nim-templates for Nim.
https://github.com/enthus1ast/nimja

compile-time compiled html html-templates jinja2 nim nim-lang template-engine twig typed

Last synced: about 1 month ago
JSON representation

typed and compiled template engine inspired by jinja2, twig and onionhammer/nim-templates for Nim.

Awesome Lists containing this project

README

        

Nimja Template Engine
=====================



typed and compiled template engine inspired by [jinja2](https://jinja.palletsprojects.com/), [twig](https://twig.symfony.com/) and [onionhammer/nim-templates](https://github.com/onionhammer/nim-templates) for Nim.

FEATURES
========

[![test](https://github.com/enthus1ast/nimja/actions/workflows/test.yml/badge.svg)](https://github.com/enthus1ast/nimja/actions/workflows/test.yml)

- compiled
- statically typed
- extends (a master template)
- control structures (if elif else / case / for / while)
- import other templates
- most nim code is valid in the templates
- very 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
```

DOCUMENTATION
=============

- this readme
- [generated nim docs](https://enthus1ast.github.io/nimja/nimja.html)
- [my blogpost about Nimja](https://blog.code0.xyz/posts/nimja/)

MOTIVATING EXAMPLE
==================

- [this example is in the example folder](https://github.com/enthus1ast/nimja/tree/master/examples/fromReadme)
- [and a more complete prologue and jester example](https://github.com/enthus1ast/nimja/tree/master/examples/prologue)
- [an example howto load templates from a shared library (dll, so)](https://github.com/enthus1ast/nimja/tree/master/examples/dynlib)

server.nim

```nim
import asynchttpserver, asyncdispatch
import nimja/parser
import os, random # os and random are later used in the templates, so imported here

type
User = object
name: string
lastname: string
age: int

proc renderIndex(title: string, users: seq[User]): string =
## the `index.nimja` template is transformed to nim code.
## so it can access all variables like `title` and `users`
## the return variable could be `string` or `Rope` or
## anything which has a `&=`(obj: YourObj, str: string) proc.
compileTemplateFile(getScriptDir() / "index.nimja")

proc main {.async.} =
var server = newAsyncHttpServer()

proc cb(req: Request) {.async.} =

# in the templates we can later loop trough this sequence
let users: seq[User] = @[
User(name: "Katja", lastname: "Kopylevych", age: 32),
User(name: "David", lastname: "Krause", age: 32),
]
await req.respond(Http200, renderIndex("index", users))

server.listen Port(8080)
while true:
if server.shouldAcceptRequest():
await server.acceptRequest(cb)
else:
poll()

asyncCheck main()
runForever()
```

index.nimja:

```twig
{% extends partials/_master.nimja%}
{#
extends uses the master.nimja template as the "base".
All the `block`s that are defined in the master.nimja are filled
with blocks from this template.

If the templates extends another, all content HAVE TO be in a block.

blocks can have arbitrary names

extend must be the first token in the template,
only comments `{# Some foo #}` and strings are permitted to come before it.
#}

{% block content %}
{# A random loop to show off. #}
{# Data is defined here for demo purpose, but could come frome database etc.. #}

Random links


{% const links = [
(title: "google", target: "https://google.de"),
(title: "fefe", target: "https://blog.fefe.de")]
%}
{% for (ii, item) in links.pairs() %}
{{ii}} This is a link to: {{item.title}}

{% endfor %}

Members


{# `users` was a param to the `renderIndex` proc #}
{% for (idx, user) in users.pairs %}
{% importnimja "./partials/_user.nimja" %}

{% endfor %}
{% endblock %}

{% block footer %}
{#
we can call arbitraty nim code in the templates.
Here we pick a random user from users.
#}
{% var user = users.sample() %}

{#
imported templates have access to all variables declared in the parent.
So `user` is usable in "./partials/user.nimja"
#}
This INDEX was presented by.... {% importnimja "./partials/_user.nimja" %}
{% endblock footer %} {# the 'footer' in endblock is completely optional #}
```

master.nimja
```twig
{#

This template is later expanded from the index.nimja template.
All blocks are filled by the blocks from index.nimja

Variables are also useable.
#}

{{title}}

body {
background-color: aqua;
color: red;
}

{# The master can declare a variable that is later visible in the child template #}
{% var aVarFromMaster = "aVarFromMaster" %}

{# We import templates to keep the master small #}
{% importnimja "partials/_menu.nimja" %}

{{title}}

{# This block is filled from the child templates #}
{%block content%}{%endblock%}

{#
If the block contains content and is NOT overwritten later.
The content from the master is rendered
#}
{% block onlyMasterBlock %}Only Master Block{% endblock %}

{% block footer %}{% endblock %}

```

partials/_menu.nimja:
```twig
index
```

partials/_user.nimja:
```twig
User: {{user.name}} {{user.lastname}} age: {{user.age}}
```

Basic Syntax
============

- `{{ myObj.myVar }}` --transformed-to---> `$(myObj.myVar)`
- {% myExpression.inc() %} --transformed-to---> `myExpression.inc()`
- {# a comment #}

How?
====

nimja transforms templates to nim code on compilation,
so you can write arbitrary nim code.
```nim
proc foo(ss: string, ii: int): string =
compileTemplateStr(
"""example{% if ii == 1%}{{ss}}{%endif%}{% var myvar = 1 %}{% myvar.inc %}"""
)
```
is transformed to:

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

this means you have the full power of nim in your templates.

USAGE
=====

there are only three relevant procedures:

- `compileTemplateStr(str: string)`
compiles a template string to nim ast
- `compileTemplateFile(path: string)`
compiles the content of a file to nim ast
- `getScriptDir()`
returns the path to your current project, on compiletime.

compileTemplateFile
-------------------

`compileTemplateFile` transforms the given file into the nim code.
you should use it like so:

```nim
import os # for `/`
proc myRenderProc(someParam: string): string =
compileTemplateFile(getScriptDir() / "myFile.html")

echo myRenderProc("test123")
```

`compileTemplateFile` can also generate an iterator body, for details look at the
iteratior section.

`compileTemplateFile` (also `compileTemplateString`) generates the body of a proc/iterator so it generates
assign calls to a variable. The default is `result`.
If you want it to use another variable set it in `varname`

also look at:
- [tmplf](#tmpls--tmplf) (inline version of this)
- [tmpls](#tmpls--tmplf)
- [compileTemplateStr](#compiletemplatestr)

compileTemplateStr
-------------------

`compileTemplateStr` compiles the given string into nim code.

```nim
proc myRenderProc(someParam: string): string =
compileTemplateStr("some nimja code {{someParam}}")

echo myRenderProc("test123")
```

`compileTemplateStr` can also generate an iterator body, for details look at the
iteratior section.

`compileTemplateString` (also `compileTemplateFile`) generates the body of a proc/iterator so it generates
assign calls to a variable. The default is `result`.
If you want it to use another variable set it in `varname`

A context can be supplied to the `compileTemplateString` (also `compileTemplateFile`), to override variable names:

```nim
block:
type
Rax = object
aa: string
bb: float
var rax = Rax(aa: "aaaa", bb: 13.37)
var foo = 123
proc render(): string =
compileTemplateString("{{node.bb}}{{baa}}", {node: rax, baa: foo})
```

Please note, currently the context **cannot be** procs/funcs etc.

also look at:
- [tmpls](#tmpls--tmplf) (inline version of this)
- [tmplf](#tmpls--tmplf)
- [compileTemplateFile](#compiletemplatefile)

if / elif / else
-----------------

```twig
{% if aa == 1 %}
aa is: one
{% elif aa == 2 %}
aa is: two
{% else %}
aa is something else
{% endif %}
```

when / elif / else
-----------------

`when` is the compile time if statement.
It has the same semantic than if

```twig
{% when declared(isDeclared) %}
isDeclared
{% elif true == true %}
true
{% else %}
something else
{% endwhen %}
```

case / of / else
-----------------
(Since Nimja 0.8.1)

`case` has the same semantic as the [nim case statement](https://nim-lang.org/docs/tut1.html#control-flow-statements-case-statement).
Use `case` for example if you want to make sure that all cases are handled.
If not all cases are covered, an error is generated.

```twig
{%- case str -%}
{%- of "foo" -%}
foo
{%- of "baa" -%}
baa
{%- of "baz" -%}
baz
{%- else -%}
nothing
{%- endcase -%}
```

tmpls / tmplf
-------------

`compileTemplateStr` and `compileTemplateFile` both need a surrounding proc.
`tmpls` (template str) and `tmplf` (template file) are a shorthand for these
situations where you want to inline a render call.

```nim
let leet = 1337
echo tmpls("foo {{leet}}")
echo tmplf(getScriptDir() / "templates" / "myfile.nimja")
```

A context can be supplied to the template, to override variable names:

```nim
block:
type
Rax = object
aa: string
bb: float
var rax = Rax(aa: "aaaa", bb: 13.37)
var foo = 123
tmpls("{{node.bb}}{{baa}}", {node: rax, baa: foo})
```

Please note, currently the context **cannot be** procs/funcs etc.

for
---

```twig
{% for (cnt, elem) in @["foo", "baa", "baz"].pairs() %}
{{cnt}} -> {{elem}}
{% endfor %}
```

```twig
{% for elem in someObj.someIter() %}
{# `elem` is accessible from the "some/template.nimja" #}
{# see importnimja section for more info #}
{% importnimja "some/template.nimja" %}
{% endfor %}
```

while
----

```twig
{% while isTrue() %}
still true
{% endwhile %}
```

```twig
{% var idx = 0 %}
{% while idx < 10 %}
still true
{% idx.inc %}
{% endwhile %}
```

comments
-------

```twig
{# single line comment #}
{#
multi
line
comment
#}
{# {% var idx = 0 %} #}
```

"to string" / output
--------------------

declare your own `$` before you call
`compileTemplateStr()` or `compileTemplateFile()`
for your custom objects.
For complex types it is recommend to use the method described in the `importnimja` section.
```twig
{{myVar}}
{{someProc()}}
```

importnimja
---------

import the content of another template.
The imported template has access to the parents variables.
So it's a valid strategy to have a "partial" template that for example
can render an object or a defined type.
Then include the template wherever you need it:

best practice is to have a `partials` folder,
and every partial template begins with an underscore "_"
all templates are partial that do not extend another
template and therefore can be included.

This way you create reusable template blocks to use all over your webpage.

partials/_user.nimja:
```twig


{{user.name}}



  • Age: {{user.age}}

  • Lastname: {{user.lastname}}



```

partials/_users.nimja:
```twig


{% for user in users: %}
{% importnimja "partials/_user.nimja" %}
{% endfor %}

```

extends
-------

a child template can extend a master template.
So that placeholder blocks in the master are filled
with content from the child.

partials/_master.nimja
```twig

A lot of boilerplate
{% block content %}{% endblock %}



{% block footer %}{% endblock %}

```

child.nimja
```
{% extends "partials/_master.nimja" %}
{% block content %}I AM CONTENT{% endblock %}
{% block footer %}...The footer..{% endblock %}
```

if the child.nimja is compiled then rendered like so:

```nim
proc renderChild(): string =
compileTemplateFile(getScriptDir() / "child.nimja")

echo renderChild()
```

output:
```html

A lot of boilerplate
I AM CONTENT



...The footer..

```

scope
--------

A `scope` has the same semantic as a nim block.
Variables declared inside a scope are not visible
on the outside. You can use this for code hygiene.

```twig
{% scope %}
{% let httpMethod = "POST" %}
{{ httpMethod }}
{% endscope %}
{# httpMethod is not accesible any more, you can define it again. #}
{% let httpMethod = "FOO" %}
{{ httpMethod }}
```

You can break out of a named scope prematurely

```
{%- scope foo -%}
foo
{%- break foo -%}
baa
{%- endscope -%}
```

in this case only "foo" is printed.

`self` variable
===============

Jinja describes them like so, we can do the same:

You can't define multiple {% block %} tags with the same name in the same template.
This limitation exists because a block tag works in "both" directions.
That is, a block tag doesn't just provide a placeholder to fill - it also defines the content that fills the placeholder in the parent.
If there were two similarly-named {% block %} tags in a template, that template's parent wouldn't know which one of the blocks content to use.

If you want to print a block multiple times, you can, however, use the special self variable and call the block with that name:

```twig
{% block title %}{% endblock %}

{{ self.title }}


{% block body %}{% endblock %}
```

To change the `specialSelf` variable name compile with eg.:

```
nim c -d:specialSelf="blocks." file.nim
```

procedures (macro)
========

Procedures can be defined like so:

```twig
{% proc foo(): string = %}
baa
{% endproc %}
{{ foo() }}
```

```twig
{% proc input(name: string, value="", ttype="text"): string = %}

{% endproc %}
{{ input("name", "value", ttype="text") }}
```

Func's have the same semantic as nim funcs, they are not allowed to have a side effect.

```twig
{% func foo(): string = %}
baa
{% endfunc %}
{{ foo() }}
```

`macro` is an alias for `proc`

```twig
{% macro textarea(name, value="", rows=10, cols=40): string = %}
{{ value }}
{% endmacro %}
{{ textarea("name", "value") }}
```

for `{{func}}` `{{proc}}` and `{{macro}}` either the `{{end}}` tag or
the `{{endfunc}}` `{{endproc}}` `{{endmacro}}` are valid closing tags.

Importing func/proc/macro from a file
------------------------------------

Importing works like any other ordinary Nimja templates with `ìmportnimja`.
Good practice is to define procs with the "whitespacecontrol":

myMacros.nimja
```
{%- proc foo(): string = %}foo{% end -%}
{%- proc baa(): string = %}baa{% end -%}
```

myTemplate.nimja
```
{% importnimja "myMacros.nimja" %}
```

When a template `extends` another template, `importnimja` statements must be
in a `block` they cannot stand on their own.
It might be a good idea to import these "library templates" in
the extended template (eg.: master.nimja).

Iterator
========

Expanded template bodies can also be created as an iterator,
therefore the generated strings are not concatenated to the result
`result &= "my string"` but are yielded.

This could be used for streaming templates, or to save memory when a big template is rendered and the http server can send data in chunks:

```nim
iterator yourIter(yourParams: bool): string =
compileTemplateString("{%for idx in 0 .. 100%}{{idx}}{%endfor%}", iter = true)

for elem in yourIter(true):
echo elem
```

Whitespace Control
==================

```twig
###############
{% if true %}

  • {{foo}}

  • {% endif %}
    ###############
    ```
    is expanded to:

    ```html
    ###############

  • FOO
  • ###############
    ```

    the nimja template control statements leave their newline and whitespace when rendered.
    To fix this you can annotate them with "-":

    ```twig
    ###############
    {% if true -%}

  • {{-foo-}}

  • {%- endif %}
    ###############
    ```

    ```html
    ###############

  • FOO

  • ###############
    ```

    Nimjautils
    ==========

    The optional `nimjautils` module, implements some convenient procedures.

    ```nim
    import nimja/nimjautils
    ```

    Mainly:

    Loop variable/iterator
    -------------

    yields a `Loop` object with every item.
    Inside the loop body you have access to the following fields.
    Unlike jinja2 or twig where the loop variable is implicitly bound and available, we must use the `loop()` iterator explicity.

    ```twig
    {% for (loop, row) in rows.loop() %}
    {{ loop.index0 }} {# which elemen (start from 0) #}
    {{ loop.index }} {# which element (start from 1) #}
    {{ loop.revindex0 }} {# which element, counted from the end (last one is 0) #}
    {{ loop.revindex }} {# which element, counted from the end (last one is 1) #}
    {{ loop.length }} {# the length of the seq, (same as mySeq.len()) #}
    {% if loop.first %}The first item{% endif %} {# if this is the first loop iteration #}
    {% if loop.last %}The last item{% endif %} {# if this is the last loop iteration #}
    {% if loop.previtem.isSome() %}{{ loop.previtem.get() }}{% endif %} {# get the item from the last loop iteration #}
    {% if loop.nextitem.isSome() %}{{ loop.nextitem.get() }}{% endif %} {# get the item from the next loop iteration #}

  • {{row}}

  • {% endfor %}
    ```

    ~~however, the element you iterate over must match the Concept `Loopable`.~~ https://github.com/enthus1ast/nimja/issues/23
    This means you can propably not use `loop()` with an iterator, since they do not have a `len()` and `[]`

    Cycle
    -----

    within a loop you can cycle through elements:

    ```twig
    {% for (loop, row) in rows.loop() %}

  • {{ row }}

  • {% endfor %}
    ```

    '~' (tilde)
    ----------

    Converts all operands into strings and concatenates them.
    like: `$aa & $bb`

    ```twig
    {{ "Hello " ~ name ~ "!" }}
    ```

    would return (assuming name is set to 'Nim') Hello Nim!.

    includeRaw
    ----------
    Includes the content of a file literally without any parsing
    Good for documentation etc..

    ```nim
    proc test(): string =
    let path = (getScriptDir() / "tests/basic" / "includeRawT.txt")
    compileTemplateStr("""pre{{ includeRaw(path) }}suf""")
    ```

    raw strings
    -----------
    to include raw strings, or nimja code itself to a template (for documentation purpose),
    you could use this construct `{{"raw code"}}`

    ```nim
    proc foo(): string =
    compileTemplateStr("""
    foo {{"{%if true%}baa{%endif%}"}}
    """)
    ```
    this would then be rendered like so:

    ```
    foo {%if true%}baa{%endif%}
    ```

    includeRawStatic
    ----------------
    Includes the content of a file literally without any parsing, on compiletime.
    This means it is included into the executable.

    includeStaticAsDataurl
    ----------------------
    Includes the content of a file on compile time, it is converted to a data url.
    Eg:

    ```html

    ```

    is transformed to:

    ```html

    ```

    truncate
    --------

    truncates a string to "num" characters.
    when the string was truncated it appends the `suf` to the text.
    if `preserveWords` is true it will not cut words in half but
    the output string could be shorter than `num` characters.

    ```nim
    proc truncate*(str: string, num: Natural, preserveWords = true, suf = "..."): string
    ```

    ```nim
    let lorem = "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Rem voluptates odio tempore voluptas beatae eum consequatur laudantium totam. Delectus fuga eveniet ab cum nulla aperiam iste ducimus odio fugit voluptas."

    proc test(lorem: string): string =
    compileTemplateStr("{{lorem.truncate(65)}}")
    assert test(lorem) == "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Rem..."
    ```

    nl2br
    -----

    Converts newline to `
    `.
    If keepNL == true, the one `\n` is replaced by `
    \n` thus keeping the newlines.

    ```nim
    func nl2br*(str: string, keepNl = true): string =
    ```

    ```nim
    assert "foo\nbaa".nl2br == "foo
    \nbaa"
    ```

    spaceless
    ---------

    Removes unneeded whitespaces between html tags,
    warning, this is NOT smart. So it will destroy `` and `

    ` content!

    ```nim
    check "\n\nbaa ".spaceless == " baa "
    check "\n\nbaa ".spaceless == " baa "
    check "baa baz".spaceless == "baa baz"
    ```

    slugify
    -------

    converts any string to an url friendly one.
    Removes any special chars and replaces non ASCII runes to their ASCII representation.

    ```nim
    slugify("Lession learned german umlauts: öüä")
    ```

    will output:

    ```lession-learned-german-umlauts-oua```

    ```nim
    let allowedCharsInSlug = Letters + Digits
    proc slugify*(str: string, sperator = "-", allowedChars = allowedCharsInSlug): string =
    ```

    shorthand if `?`
    ----------------

    a shorthand for a condition, this could be used for example
    to toggle html classes:

    ```nim
    proc foo(isDisabled: bool): string =
    compileTemplateStr("""{% ?isDisabled: "disabled" %}""")
    check "disabled" == foo(true)
    check "" == foo(false)
    ```

    filter `|`
    ---------

    `a | b` is an alias to `a.b` this is often used in other template engines.

    ```nim
    proc foo(): string =
    compileTemplateStr("""{{"foo baa baz" | slugify}}""")
    check foo() == "foo-baa-baz"
    ```

    Want to hack?
    -------------
    > if you need more utils in nimjautils, please PR!
    > they should all be quite easy to implement,
    > so they make up a good first issue/pull request!
    >
    >a good inspiration WHAT to hack is jinja and twig filters.

    Compile / Use
    =============

    This is a COMPILED template engine.
    This means you must _recompile_ your application
    for every change you do in the templates!

    ~~_Automatic recompilation / hot code reloading / dynamic execution is a [planned feature](https://github.com/enthus1ast/nimja/issues/6)._~~ see the
    `Automatic Recompilation / Hot Code Reloading (hcr)` section

    ```bash
    nim c -r yourfile.nim
    ```

    sometimes, nim does not catch changes to template files.
    Then compile with "-f" (force)

    ```bash
    nim c -f -r yourfile.nim
    ```

    Automatic Recompilation / Hot Code Reloading (hcr)
    ============================================

    (Still an experimental feature, help wanted.)
    Automatic Recompilation enables you to change your templates and without
    recompiling your application, see the changes lives.

    How it works:

    Nimja compiles your templates (and template render functions)
    to a shared library (.so/.dll/.dynlib), then your host application loads
    this library, then on source code change, the shared library is unloaded from
    your host, recompiled, and loaded again.

    This is normally way faster, than recompiling your whole application.

    For this to work, Nimja now contains a small file watcher, you must utilize this
    tool in your own application.

    You also must restructure you application a little bit,
    all you render functions must be in a separate file,
    this file is then compiled to a shared lib and loaded by your host.

    When you go live later, you can just disable the recompilation,
    and compile the shared library for release, it should be very fast as well.

    Below is a minimal example, [a more complete example is in the example folder](https://github.com/enthus1ast/nimja/tree/master/examples/hcr)

    Minimal example:

    `host.nim`

    ```nim
    # this is the file that eg. implements your webserver and loads
    # the templates as a shared lib.
    import nimja/hcrutils # Nimja's hot code reloading utilities
    import jester, os

    # We watch the templates folder for change (and also tmpls.nim implicitly)
    var cw = newChangeWatcher(@[getAppDir() / "templates/"])
    asyncCheck cw.recompile() # if a change is detected we recompile tmpls.nim

    type
    # You must declare the proc definition from your tmpls.nim here as well.
    ProcNoParam = proc (): string {.gcsafe, stdcall.}
    ProcId = proc (id: string): string {.gcsafe, stdcall.}

    routes:
    get "/":
    resp dyn(ProcNoParam, "index")

    get "/id/@id":
    resp dyn(ProcId, "detail", @"id")
    ```

    `tmpls.nim`

    ```nim
    # this file contains you render functions
    # is compiled to a shared lib and loaded by your host application
    # to keep compilation fast, use this file only for templates.
    # this file is also watched by the filewatcher.
    # It can also be changed dynamically!
    import nimja
    import os # for `/`

    proc index*(): string {.exportc, dynlib.} =
    var foos = 1351 # change me i'm dynamic :)
    compileTemplateFile(getScriptDir() / "templates/index.nimja")

    proc detail*(id: string): string {.exportc, dynlib.} =
    compileTemplateFile(getScriptDir() / "templates/detail.nimja")

    ```

    `templates/`

    `templates/partials/_master.nimja`

    ```html

    Hello, world!

    Nimja dynamic test



    {% block content %}{% endblock %}