Crate neutralts

Source
Expand description

neutral

§Rust Web Template Engine

Neutral is a template engine for the Web, designed to work with any programming language (language-agnostic) via IPC and natively as library/crate in Rust.

In this simple PWA example, all three use exactly the same templates.

(*) For non-Rust requires an IPC server that you can download from the IPC repository - IPC server

The documentation of the web template engine is here: template engine doc and Rust documentation here: rust doc.

§Rust

In Rust it is enough with two methods, create the template with a file and a schema and then render:

// Data
let schema = json!({
    "data": {
        "hello": "Hello, World!",
        "site": {
            "name": "My Site"
        }
    }
});

// Create template
// In file.ntpl use {:;hello:} and {:;site->name:} for show data.
let template = Template::from_file_value("file.ntpl", schema).unwrap();

// Render template
let content = template.render();

§Syntax

The main element of Neutral TS is the BIF (Build-in function), would be the equivalent of functions and would display an output, the output is always a string or nothing (empty string).

§Bif layout


 .-- open bif
 |     .-- bif name
 |     |               .-- params
 |     |               |             .-- code
 |     |               |             |   .-- close bif
 |     |               |             |   |
 v     v               v             v   v
 -- ------- --------------------  ------ --
 {:!include; {:flg; ... :} file >>  ...  :}
   -       - -------------      --
   ^       ^      ^             ^
   |       |      |             |
   |       |      |             ·-- params/code separator
   |       |      ·-- flags
   |       ·-- name separator
   ·-- modifier

                         .-- source
                         |
                         v
             ----------------------------
 {:!include; {:flg; ... :} file >>  ...  :}
 ------------------------------------------
                     ^
                     |
                     ·-- Bif (Build-in function)

Bif example:

{:filled; varname >>
    Hello!
:}

Sometimes they only carry parameters or code:

{:snippet; snipname :}

Any number of spaces can be used to separate each part of a bif except the name, the following is an error:

-{: filled ; varname >>
    Hello!
:}

+{:filled; varname >>
    Hello!
:}

Any other is valid:

{:filled;varname>>Hello!:}

{:filled;

    varname

    >>

    Hello!

:}

{:filled; varname >>
    Hello!
:}

§Variables

Variables are defined in the schema in the data key or in separate files that are loaded with {:data; ... :}

{
    "config": {},
    "inherit": {
        "locale": {
            "current": "en",
            "trans": {}
        }
    },
    "data": {
        "site_name": "MySite",
        "site": {
            "name": "MySite"
        }
    }
}

Then all we have to do is {:;varname:}:

{:;site_name:}

Output:

MySite

Arrays with the “->” operator

{:;site->name:}

Output:

MySite

§Nesting

By design all Bifs can be nested and there can be a Bif anywhere in another Bif except in the name.

{:eval; {:code; {:code; ... :} :} >>
    {:code;
        {:code;
            ...
        :}
    :}
:}

§Grouping

{:coalesce;
    {:code; {:* block 1 *:}
        {:code; ... :}
        {:code; ... :}
        {:code; ... :}
    :}
    {:code; {:* block 2 *:}
        {:code; ... :}
    :}
:}

§Wrapping

Note the following example:

<li>{:snippet; snippet-name :}</li>

The output of the above if “snippet-name” is empty will be:

<li></li>

To prevent this from happening, it can be done:

{:eval; {:snippet; snippet-name :} >>
    <li>{:;__eval__:}</li>
:}

§Control flow

The following would be a form of flow control:

{:filled; varname >>
    {:snippet; snipname :}
:}

Another:

{:snippet; foo >>
    ...
:}

{:snippet; bar >>
    ...
:}

{:same; /{:;varname:}/foo/ >>
    {:snippet; foo :}
:}

{:same; /{:;varname:}/bar/ >>
    {:snippet; var :}
:}

This is clearer and less computationally expensive:

{:snippet; option-foo >>
    ...
:}

{:snippet; option-bar >>
    ...
:}

{:snippet; option-{:;varname:} :}

Calling a snippet that does not exist is not an error, it will result in a empty string that we can evaluate with else.

{:snippet; option-{:;varname:} :}
{:else; {:snippet; option-default :} :}

§Safety

By design the templates do not have access to arbitrary application data, the data has to be passed to the template in a JSON, then the data that you have not included in the JSON cannot be read by the template.

§Variables

By design the value of the variables is not evaluated:

{
    "data": {
        "inject": "{:exit;:}",
    }
}

Then:

<div>{:;inject:}</div>
<div>{:eval; {:;inject:} >> {:;__eval__:} :}</div>
<div>{:code; {:;inject:} :}</div>
<div>{:code; {:code; {:;inject:} :} :}</div>

In no case is {:exit;:} evaluated, output:

<div>{:exit;:}</div>
<div>{:exit;:}</div>
<div>{:exit;:}</div>
<div>{:exit;:}</div>

This is especially important when someone tries to do this:

{
    "data": {
        "inject": "{:include; /path/to/secrets :}"
    }
}

When cache_disable = false on all values, possible bifs are filtered:

<div>{:;inject:}</div>
<div>{:eval; {:;inject:} >> {:;__eval__:} :}</div>
<div>{:code; {:;inject:} :}</div>
<div>{:code; {:code; {:;inject:} :} :}</div>

Output:

<div>&#123;:exit;:&#125;</div>
<div>&#123;:exit;:&#125;</div>
<div>&#123;:exit;:&#125;</div>
<div>&#123;:exit;:&#125;</div>

Note the following example:

{
    "data": {
        "secret": "123456",
        "reference": "secret"
    }
}

The following will produce the error insecure varname:

{:; {:;reference:} :}

To evaluate a complete variable you must use allow or evaluate partially:

{:; anything-{:;reference:} :}

The reason it can be partially evaluated is to be able to do this:

{:; array->{:;key:} :}

In the same way that you can do something similar in any programming language:

$array[$key]

In this case you should take precautions or use allow, and as a rule, NEVER evaluate variables that come from the user, GET, POST, COOKIES, ENV, … if you are not using allow:

{:declare; valid >>
    word1
    word2
:}

{:;
    {:allow; valid >> {:;reference:} :}
:}

§{:code; … :}

Unsafe variables can be displayed in a code block:

{
    "data": {
        "inject": "<div>{:exit;:}</div>",
    }
}

Then:

{:;inject:}
{:code; {:flg; safe :} >> {:;inject:} :}
{:code; {:flg; encode_tags_after :} >> {:;inject:} :}

Output:

<div>{:exit;:}</div>
&#123;:;inject:&#125;
&lt;div&gt;{:exit;:}&lt;&#x2F;div&gt;

§Files

The following will produce the error insecure file name:

{:include; {:;varname:} :}

To evaluate a complete variable you must use allow or evaluate partially:

{:include; anything-{:;varname:} :}

It is best to always use allow in include when using a variable in the file name, including the case of partial evaluation, and as a rule, NEVER evaluate variables that come from the user, GET, POST, COOKIES, ENV, … if you are not using allow:

{:declare; valid-files >>
    home.ntpl
    login.ntpl
    error.ntpl
:}

{:include;
    {:allow;
        valid-files >> {:;varname:}
    :}{:else;
        error.ntpl
    :}
:}

§Cross-Site Scripting (XSS)

There is a space reserved in the schema for variables coming from the user:

{
    "config": {},
    "inherit": {},
    "data": {
        "CONTEXT": {
            "ROUTE": "",
            "HOST": "",
            "GET": {},
            "POST": {},
            "HEADERS": {},
            "REQUEST": {},
            "FILES": {},
            "COOKIES": {},
            "SESSION": {},
            "ENV": {}
        }
    }
}

All CONTEXT variables are filtered automatically:

& → &amp;
< → &lt;
> → &gt;
" → &quot;
' → &#x27;
/ → &#x2F;
{ → &#123;
} → &#125;

However, you must take care of assigning the variables coming from the user to CONTEXT in your application. This way of proceeding allows the templates to know the insecure variables, moreover, they can be identified at a glance in the code.

The variable names is as unsafe as its value and is filtered by default.

There is also a schema configuration to escape all variables filter_all:

{
    "config": {
        "filter_all": true
    },
    "inherit": {},
    "data": {}
}

Default is false.

§Rules

  • Never trust on the context: GET, POST, COOKIES, ENV, …
  • In the application never trust that the templates take care of security.
  • In the templates never trust that the application is in charge of security.

Act in your application as if the templates were insecure and filter all variables coming from the user and from insecure sources, do the same in your template, filter all variables from insecure sources.


§schema

The schema is a JSON where we define the data that will represent the templates, as well as the configuration and translations, although you can render a template without schema since Neutral TS provides one by default, it will be of little use since it has no data.

{
    "config": {
        "comments": "remove",
        "cache_prefix": "neutral-cache",
        "cache_dir": "",
        "cache_on_post": false,
        "cache_on_get": true,
        "cache_on_cookies": true,
        "cache_disable": false,
        "filter_all": false,
        "disable_js": false
    },
    "inherit": {
        "locale": {
            "current": "en",
            "trans": {
                "en": {
                    "Hello nts": "Hello",
                    "ref:greeting-nts": "Hello"
                },
                "es": {
                    "Hello nts": "Hola",
                    "ref:greeting-nts": "Hola"
                },
                "de": {
                    "Hello nts": "Hallo",
                    "ref:greeting-nts": "Hallo"
                },
                "fr": {
                    "Hello nts": "Bonjour",
                    "ref:greeting-nts": "Bonjour"
                },
                "el": {
                    "Hello nts": "Γεια σας",
                    "ref:greeting-nts": "Γεια σας"
                }
            }
        }
    },
    "data": {
        "CONTEXT": {
            "ROUTE": "",
            "HOST": "",
            "GET": {},
            "POST": {},
            "HEADERS": {},
            "FILES": {},
            "COOKIES": {},
            "SESSION": {},
            "ENV": {}
        },
        "web-site-name": "MySite",
        "varname": "value",
        "true": true,
        "false": false,
        "hello": "hello",
        "zero": "0",
        "one": "1",
        "spaces": "  ",
        "empty": "",
        "null": null,
        "emptyarr": [],
        "array": {
            "true": true,
            "false": false,
            "hello": "hello",
            "zero": "0",
            "one": "1",
            "spaces": "  ",
            "empty": "",
            "null": null
        }
    }
}

§Rust

let schema = json!({
    "inherit": {
        "locale": {
            "current": "en"
        }
    },
    "data": {
        "web_site_name": "MySite"
    }
});

let template = Template::from_file_value("file.ntpl", schema).unwrap();
let content = template.render();

§Scope and inheritance

By default the scope of the definitions is block inheritable to the children of the block:

   {:code; <--------------------------.
       {:* block *:}                  |<---- Block
       {:param; name >> value :} <----|----- Set "name" for this block and its children
       {:param; name :} <-------------|----- "name" has the value "value".
       {:code;                        |
           {:* child block *:}        |
           {:param; name :} <---------|----- "name" has the value "value".
       :}                             |
   :} <-------------------------------·
   {:param; name :} <----------------------- outside block, no value or a previous value if any.

“include” has a block scope, then:

   {:code;
       {:* include for set "snippet-name" *:}
       {:include; snippet.ntpl :}
       {:snippet; snippet-name :} {:* Ok, "snippet-name" is set *:}
   :}
   {:snippet; snippet-name :} {:* Ko, "snippet-name" is not set *:}

The modifier scope (+) adds the scope to the current level

   {:+code;
       {:* include for set "snippet-name" *:}
       {:include; snippet.ntpl :}
       {:snippet; snippet-name :} {:* Ok, "snippet-name" is set *:}
   :}
   {:snippet; snippet-name :} {:* Ok, "snippet-name" is set *:}

To make it possible to do this:

   {:bool; something >>
       {:include; snippet.ntpl :}
   :}
   {:snippet; snippet-name :} {:* Ko, "snippet-name" is not set *:}

   {:+bool; something >>
       {:include; snippet.ntpl :}
   :}
   {:snippet; snippet-name :} {:* Ok, "snippet-name" is set *:}

§Modifiers

Modifiers allow changing the behavior of the bif and each bif may or may not support a certain modifier:

^ upline
! not
+ scope
& filter

Example:

{:^defined; varname >> ... :}
{:!defined; varname >> ... :}
{:+defined; varname >> ... :}
{:&;varname:}

Two or more modifiers can be combined, regardless of the order:

{:^!defined; varname >> ... :}

§Modifier: ^ (upline)

Eliminates previous whitespaces.

§Modifier: ! (not)

They usually negate, but may have less obvious behavior as in the case of bif trans.

{:defined; varname >> ... :}   {:* if varname is defined *:}
{:!defined; varname >> ... :}  {:* if varname is not defined *:}

{:trans; text :}               {:* translates the text or outputs text if no translation *:}
{:!trans; text :}              {:* translates the text or outputs empty if no translation *:}

{:exit; 302 :}                 {:* Terminate and set 302 status *:}
{:!exit; 302 :}                {:* Set 302 status and continues executing *:}

§Modifier: + (scope)

By default the scope of the definitions is block inheritable to the children of the block:

{:code; <--------------------------.
    {:* block *:}                  |<---- Block
    {:param; name >> value :} <----|----- Set "name" for this block and its children
    {:param; name :} <-------------|----- "name" has the value "value".
    {:code;                        |
        {:* child block *:}        |
        {:param; name :} <---------|----- "name" has the value "value".
    :}                             |
:} <-------------------------------·
{:param; name :} <----------------------- outside block, no value or a previous value if any.

“include” has a block scope, then:

{:code;
    {:* include for set "snippet-name" *:}
    {:include; snippet.ntpl :}
    {:snippet; snippet-name :} {:* Ok, "snippet-name" is set *:}
:}
{:snippet; snippet-name :} {:* Ko, "snippet-name" is not set *:}

The modifier scope (+) adds the scope to the current level

{:+code;
    {:* include for set "snippet-name" *:}
    {:include; snippet.ntpl :}
    {:snippet; snippet-name :} {:* Ok, "snippet-name" is set *:}
:}
{:snippet; snippet-name :} {:* Ok, "snippet-name" is set *:}

To make it possible to do this:

{:bool; something >>
    {:include; snippet.ntpl :}
:}
{:snippet; snippet-name :} {:* Ko, "snippet-name" is not set *:}

{:+bool; something >>
    {:include; snippet.ntpl :}
:}
{:snippet; snippet-name :} {:* Ok, "snippet-name" is set *:}

§Modifier: & (filter)

Escapes special HTML characters and braces:

& → &amp;
< → &lt;
> → &gt;
" → &quot;
' → &#x27;
/ → &#x2F;
{ → &#123;
} → &#125;

§{:* comment *:}

{:* This is a comment *:}

{:*
    This is a comment
    -----------------
*:}

{:*
    Nested comment
    --------------
    {:* comment
        {:* comment
            {:* ... *:}
        *:}
    *:}
*:}

Comment inside build-in function:

{:; varname {:* comment *:} :}

{:code; {:* comment *:}
    {:param; parvalue {:* comment *:} :}
    {:param; parvalue :} {:* comment *:}
    ...
:}

The only place where we cannot insert a comment is the bif name, any other location is acceptable.


§{:; … :} (var)

Output var value.

{:;varname:}
{:;array->key:}

§Modifiers:

{:^;varname:}
{:&;varname:}
{:!;varname:}

§Modifier: ^ (upline)

Assuming that the value of “varname” is “value”:

<div></div>

{:;varname:}

<div></div>

{:^;varname:}

Output:

<div></div>

value

<div></div>value

§Modifier: & (filter)

Escapes special HTML characters and braces:

& → &amp;
< → &lt;
> → &gt;
" → &quot;
' → &#x27;
/ → &#x2F;
{ → &#123;
} → &#125;

By default all user variables are filtered, those starting with CONTEXT->. There is also a schema configuration to escape all variables filter_all:

{
    "config": {
        "filter_all": true
    },
    "inherit": {},
    "data": {}
}

Default is false.

§Modifier: ! (not)

Does not filter special HTML characters and braces, if combined with & the result is no filtering.

Avoid filtering a variable:

{:!;CONTEXT->GET->var:}

The contradictory combination with & results in no filtering:

{:&!;varname:}

§No flags

§Arrays

To access an array, use: “->”, no distinction between objects and arrays. Assuming:

{
    "data": {
        "arr": [
            "value"
        ],
        "obj": {
            "0": "value"
            "arr": [
                "value"
            ],
        }
    }
}

Then:

{:;arr->0:}
{:;obj->0:}
{:;obj->arr->0:}

§Dynamic evaluation

{:;array->{:;key:}:}

However, the following will produce an error:

{:;{:;varname:}:}

For safety reasons, when evaluating the complete variable it is necessary to use “allow”:

{:; {:allow; allowed-words-list >> {:;varname:} :} :}

In any case, you must use “allow” on any variable that comes from the context. See the “allow” and “declare” bifs for more details.

§Undefined

It is not an error to use an undefined variable or an array, nor will it show any warning, in the case of an array it will show an empty string:

<div>{:;undefvar:}</div>
<div>{:;array:}</div>

Output:

<div></div>
<div></div>

§{:;:} (unprintable)

Output empty string, eliminates or preserves spaces.

{:;:}

§Modifiers

{:^;:}

§Modifier: ^ (upline)

Eliminates previous whitespaces.

Assuming varname = Hello:

<div></div>

{:;varname:}

<div></div>

{:^;:}{:;varname:}

Output:

<div></div>

Hello

<div></div>Hello

Assuming varname = Hello:

-|
{:;:}

{:^;:}{:;varname:}
-|

{:^;:}{:;varname:}

Output:

-|
Hello
-|Hello

§No flags

§Usage

The following are the same:

<div>{:;:}</div>
<div>{:; :}</div>
<div>{:;     :}</div>
<div>{:;

:}</div>

Output:

<div></div>
<div></div>
<div></div>
<div></div>

The usual behavior in output is as expected in practically all cases, the following produce the same output:

<div>{:code;Hello:}</div>
<div>{:code; Hello :}</div>
<div>{:code;

    Hello

:}</div>

Output:

<div>Hello</div>
<div>Hello</div>
<div>Hello</div>

But in special cases we may need to make it so that spaces or carriage returns are not removed. And this is the main use of unprintable Bif:

<pre>
{:code;
    {:^;:}
    Hello
    {:^;:}
:}
</pre>

Output:

<pre>
    Hello
</pre>

Preserve space:

<div>{:code;   Hello   :}</div>
<div>{:code; {:;:} Hello :}</div>
<div> {:code; Hello :}</div>

Output:

<div>Hello</div>
<div> Hello</div>
<div> Hello</div>

In the previous example:

                  .--- preserve
                  |
                  v
<div>{:code; {:;:} Hello :}</div>

                  .--- preserve two
                  ||
                  vv
<div>{:code; {:;:}  Hello :}</div>

Not preserve spaces:

<div>
    {:code;
        Hello
    :}
</div>
<div>{:;
    :}{:code;
        Hello
    :}{:;
:}</div>

Output:

<div>
    Hello
</div>
<div>Hello</div>

§{:allow; … :}

Output literal if the literal found from a list defined with “declare”, empty string if it fails.

{:allow; declare-name >> literal :}

{:allow; allowed-words-list >> {:;varname:} :}

In the above the output will be the content of varname, if that content is supported by allowed-words-list.

It is mainly used for safety and its most common use is this one:

{:include; {:allow; allowed-words-list >> {:;varname:} :} :}
{:include; {:!allow; traversal >> {:;varname:} :} :}

In the first case it only supports values that reside in the word list, preventing arbitrary files from being accessed. The second case does not allow directory traversal.

It is convenient to set a default value, as failure to do so will output an empty string:

{:include;
    {:allow;
        allowed-words-list >> {:;varname:}
    :}{:else:
        secure or default value
    :}
:}

Or:

{:include;
    {:allow;
        allowed-words-list >> {:;varname:}
    :}{:else:
        {:exit; 403 :}
    :}
:}

§Modifiers:

{:!allow; ... :}
{:^allow; ... :}

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§Modifier: ! (not)

Output of an empty string in case it is found in the “declare”.

§Flags

{:allow; {:flg; partial casein replace :} name >> ... :}

§Flag: partial

It would be the equivalent of having wildcards in the word list, from “word” to “word”.

§Flag: casein

Case insensitive

§Flag: replace

Returns the word found (without wildcards) instead of the evaluation text.

§Word declaration

With “declare” we define the list of words to be allowed or denied:

{:declare; files >>
    home.tpl
    contact.tpl
    about.tpl
:}

{:declare; traversal >>
    /*
    \\\\*
    *\\.\\.*
:}

Then, the following will produce an error:

{:include;
    {:allow;
        files >> passwd
    :}{:else;
        {:exit; 403 :}
    :}
:}

An error will also occur here:

{:include;
    {:!allow;
        traversal >> ../dir/file
    :}{:else;
        {:exit; 403 :}
    :}
:}

The most successful method to do this is the first one, where only the declared files are supported.

Declare supports wildcards, see bif “declare” for details.

§Examples

Assumes:

{:*
    Allow any template file
*:}
{:declare; templates >>
    *.ntpl
:}

Then:

<div>{:allow; templates >> file.txt :}{:else; fails :}</div>
<div>{:allow; templates >> file.ntpl :}{:else; fails :}</div>

Output:

<div>fails</div>
<div>file.ntpl</div>

Assumes:

{:*
    Allow languages
*:}
{:declare; languages >>
    en
    en-??
    en_??
    es
    es-??
    es_??
:}

Then:

<div>{:allow; languages >> fr :}{:else; fails :}</div>
<div>{:allow; languages >> es-ES :}{:else; fails :}</div>

Output:

<div>fails</div>
<div>es-ES</div>

Assumes:

    {:*
        Allow languages
    *:}
    {:declare; languages >>
        en
        en???
        es
        es???
    :}

Then:

    <div>{:allow; {:flg; replace :} languages >> de :}{:else; en :}</div>
    <div>{:allow; {:flg; replace :} languages >> es-ES :}{:else; en :}</div>

Output:

    <div>en</div>
    <div>es</div>

§{:array; … :}

Output code if a variable is array.

{:array; variable >> code :}

{:array; varname >> this shown if varname is array :}

Note that there is no distinction between objects and arrays.

§Modifiers:

{:!array; ... :}
{:+array; ... :}
{:^array; ... :}

For more details about the “+” modifier see “modifiers”.

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§Modifier: not

{:!array; varname >> this shown if varname is not array :}

§No flags

§Examples

{:array; true          >> Not shown :}
{:array; false         >> Not shown :}
{:array; hello         >> Not shown :}
{:array; zero          >> Not shown :}
{:array; one           >> Not shown :}
{:array; spaces        >> Not shown :}
{:array; empty         >> Not shown :}
{:array; null          >> Not shown :}
{:array; undef         >> Not shown :}
{:array; undefarr      >> Not shown :}
{:array; emptyarr      >> Shown! :}
{:array; array         >> Shown! :}
{:array; array->true   >> Not shown :}
{:array; array->false  >> Not shown :}
{:array; array->hello  >> Not shown :}
{:array; array->zero   >> Not shown :}
{:array; array->one    >> Not shown :}
{:array; array->spaces >> Not shown :}
{:array; array->empty  >> Not shown :}
{:array; array->null   >> Not shown :}
{:array; array->undef  >> Not shown :}

Assumes data:

{
    "data": {
        "true": true,
        "false": false,
        "hello": "hello",
        "zero": "0",
        "one": "1",
        "spaces": "  ",
        "empty": "",
        "null": null,
        "emptyarr": [],
        "array": {
            "true": true,
            "false": false,
            "hello": "hello",
            "zero": "0",
            "one": "1",
            "spaces": "  ",
            "empty": "",
            "null": null
        }
    }
}

§{:bool; … :}

Output code if a variable is true

{:bool; variable >> code :}

{:bool; varname >> this shown if varname is true :}

§Modifiers:

{:!bool; varname >> ... :}
{:^bool; varname >> ... :}

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§Modifier: ! (not):

{:!bool; varname >> this shown if varname is false :}

§No flags

§Examples

{:bool; true          >> Shown! :}
{:bool; false         >> Not shown :}
{:bool; hello         >> Shown! :}
{:bool; zero          >> Not shown :}
{:bool; one           >> Shown! :}
{:bool; spaces        >> Shown! :}
{:bool; empty         >> Not shown :}
{:bool; null          >> Not shown :}
{:bool; undef         >> Not shown :}
{:bool; undefarr      >> Not shown :}
{:bool; emptyarr      >> Not shown :}
{:bool; array         >> Shown! :}
{:bool; array->true   >> Shown! :}
{:bool; array->false  >> Not shown :}
{:bool; array->hello  >> Shown! :}
{:bool; array->zero   >> Not shown :}
{:bool; array->one    >> Shown! :}
{:bool; array->spaces >> Shown! :}
{:bool; array->empty  >> Not shown :}
{:bool; array->null   >> Not shown :}
{:bool; array->undef  >> Not shown :}

Assumes data:

{
    "data": {
        "true": true,
        "false": false,
        "hello": "hello",
        "zero": "0",
        "one": "1",
        "spaces": "  ",
        "empty": "",
        "null": null,
        "emptyarr": [],
        "array": {
            "true": true,
            "false": false,
            "hello": "hello",
            "zero": "0",
            "one": "1",
            "spaces": "  ",
            "empty": "",
            "null": null
        }
    }
}

§{:cache; … :}

Output code an store in cache. It is a modular cache with the option to exclude parts of the cache.

{:cache; /expires/id/only_custom_id/ >> code :}
{:cache; /expires/id/ >> code :}
{:cache; /expires/ >> code :}
{:!cache; code :} {:* exclude from cache *:}
  • expires: Seconds of life in the cache
  • id: Add a literal to the cache ID
  • only_custom_id: Use only the ID passed as ID,

The only mandatory parameter is expires, the cache automatically generates an ID with context data, such as language, cookies, … and code.

With the id parameter you can add a literal to the ID that is automatically generated, or use just the id provided by adding a boolean true value to the third parameter:

{:cache; /60/my-id/1/ >>
    {:code; foo :}
:}

The previous example replaces the automatic ID and this one adds:

{:cache; /60/my-id/ >>
    {:code; foo :}
:}

Any delimiter can be used:

{:cache; /expires/ >> ... :}
{:cache; ~expires~ >> ... :}
{:cache; #expires# >> ... :}
{:cache; |expires| >> ... :}

§Example

In this example the first date will be the same on every run for 300 seconds, the second date in the !cache block will not be included in the cache and will show a different date on every run.

{:cache; /300/ >>
    <!DOCTYPE html>
    <html>
        <head>
            <title>Cache</title>
        </head>
        <body>
            <main>
                <div>In cache:{:date; %H:%M:%S :}</div>
                <div>No cache:{:!cache; {:date; %H:%M:%S :} :}</div>
            </main>
        </body>
    </html>
:}

With the cache ID a hash is generated and this will be the name of the file in the cache, by including the code in the hash each cache block with different code will generate a different cache file. This will generate two different caches without the need to specify additional ID:

{:cache; /120/ >>
    {:code; foo :}
:}
{:cache; /120/ >>
    {:code; bar :}
:}

These two blocks of code being the same will have the same file in the cache:

{:cache; /120/ >>
    {:code; foo :}
:}

...

{:cache; /120/ >>
    {:code; foo :}
:}

expires is included in the ID, so these two blocks of code are distinct:

{:cache; /60/ >>
    {:code; foo :}
:}
{:cache; /120/ >>
    {:code; foo :}
:}

§Modifiers:

{:^cache; ... :}
{:!cache; ... :}

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§Modifier: ! (not)

Excludes parts of the cache, the “bar” block is excluded from the cache:

{:cache; /300/ >>
    {:code; foo :}
    {:!cache; {:code; bar :} :}
:}

You can use !cache outside a cache block to prevent that code from not being included in the cache in the future or by a parent:

<!DOCTYPE html>
<html>
    <head>
        <title>Cache</title>
    </head>
    <body>
        <main>
            <div>{:!cache; {:code; foo :} :}</div>
        </main>
    </body>
</html>

§No flags

§Nesting

It can be nested, either cache or !cache or a mixture of both:

{:cache; /20/ >>
    {:!cache;
        {:date; %H:%M:%S :}
        {:cache; /20/ >>
            {:!cache;
                {:date; %H:%M:%S :}
            :}
        :}
    :}
:}

§Config

{
    "config": {
        "cache_prefix": "neutral-cache",
        "cache_dir": "",
        "cache_on_post": false,
        "cache_on_get": true,
        "cache_on_cookies": true,
        "cache_disable": false
    },
    "inherit": {
        "locale": {
            "current": "en"
        }
    },
    "data": {
        "CONTEXT": {
            "ROUTE": "",
            "HOST": "",
            "GET": {},
            "POST": {},
            "HEADERS": {},
            "FILES": {},
            "COOKIES": {},
            "SESSION": {},
            "ENV": {}
        }
    }
}

The language is included in the cache ID, so there will always be different versions of the cache for each language, the language is defined in inherit.locale.current.

Various options can be set in the configuration:

  • cache_prefix: Adds a prefix, one more level of cache directory.
  • cache_dir: Directory where the cache files are stored, if empty, the temporary system directory.
  • cache_on_post: Cache POST method, default false.
  • cache_on_get: Cache GET method, default true.
  • cache_on_cookies: Cache when cookies are present, default true.
  • cache_disable: Completely disable the cache, default false.

When cache_disable = false on all values, possible bifs are filtered.

In order for Neutral TS to detect POST, GET and cookies, in your application you will have to fill the schema variable in data called CONTEXT with the POST, GET or cookie data.

Since POST requests are almost all different, it is useless to cache the POST method.

When GET or cookies are cached, different versions of the cache are generated for each variable or combination included in GET or cookies, using the automatic cache ID.

§Examples

If we run the following:

{:cache; /300/ >>
    In cache: {:date; %S :}
    No cache: {:!cache; {:date; %S :} :}
:}

The first time will have the output:

In cache: 30
No cache: 30

If we run it a second later, the cache part will be the same for 300 seconds, while the !cache part is always updated.

In cache: 30
No cache: 31

Nesting:

{:cache; /100/ >>
    {:date; %H:%M:%S :}
    {:cache; /300/ >>
        {:date; %H:%M:%S :}
        {:!cache; {:date; %H:%M:%S :} :}
    :}
:}

In the above example there is a global cache of 100 seconds and a nested cache of 300 seconds, the first date will be updated every 100 seconds and the second every 300 seconds. The last one in the !cache block will always be updated.

If the expiry of the nested cache were equal to or less than that of its parent, it would have no effect, they would be updated at the same time.

§clean-cache

There is a bash utility here: cache-utils which can be run periodically to remove old cache files:

clean-cache /tmp/neutral-cache

The parameter is the cache directory, for security reasons the directory must contain the text “neutral-cache” to avoid deleting things that are not in the cache, also only the subdirectories and files that have the Neutral TS format are deleted.


§{:coalesce; … :}

Output the first non-empty (non-zero length) of a block list. Nothing may be displayed if all blocks are empty.

{:coalesce;
    {:;varname1:}
    {:;varname2:}
    {:;varname3:}
:}

{:coalesce;
    {:code;
        {:code; ... :}
        {:code; ... :}
        {:code; ... :}
    :}
    {:code;
        {:code; ... :}
    :}
    {:;varname:}
:}

§Modifiers:

{:+coalesce; varname >> ... :}
{:^coalesce; varname >> ... :}

For more details about the “+” modifier see “modifiers”.

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§No flags

§Nesting

Can be nested (no limit):

{:coalesce;
    {:coalesce;
        {:code; ... :}
        {:code; ... :}
        {:code; ... :}
    :}
    {:code;
        {:code; ... :}
    :}
:}

§Blocks

Each block can be a single Bif, or a nested set of them:

                {:coalesce;
              .---- {:code;
              |         {:code; ... :}
    Block --> |         {:code; ... :}
              |         {:code; ... :}
              `---- :}
              .---- {:code;
    Block --> |         {:code; ... :}
              `---- :}
    Block --------> {:;varname:}
                :}

§Spaces

A variable with spaces is not a zero length string, the following example will show the contents of spaces:

{:coalesce;
    {:;spaces:}
    {:code;
        Hello
    :}
:}

In reality, spaces will not be displayed, the output will be empty, since the default behavior of neutral is to ignore spaces.

If the variable with spaces was nested, then the result would be an empty block, the following will show “Hello”:

{:coalesce;
    {:code;
        {:;spaces:}
    :}
    {:code;
        Hello
    :}
:}

Unprintable Bif {:;:} is not a zero length string, the following will show nothing:

{:coalesce;
    {:code;
        {:;:}
    :}
    {:code;
        Hello
    :}
:}

Do not show anything if a condition is given:

{:coalesce;
    {:defined; varname >>
        {:;:}
    :}
    {:code;
        Hello
    :}
:}

§{:code; … :}

Output code without action.

In some cases we need a block of code, “code” does nothing, simply encloses in block. It also serves as a wrapper to create parameters.

{:code; code :}

{:code; This is always shown :}

§Modifiers:

{:+code; ... :}
{:^code; ... :}

For more details about the “+” modifier see “modifiers”.

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§Flags

{:code; {:flg; safe noparse encode_tags encode_bifs encode_tags_after :} >> ... :}

§Flag: noparse

Prevent the block’s bifs from being parsed, assuming that the value of varname is value:

{:code;
    <div>{:;varname:}</div>
:}
{:code; {:flg; noparse :} >>
    <div>{:;varname:}</div>
:}

Output:

<div>value</div>
<div>{:;varname:}</div>

§Flag: encode_tags

Encoding html tags, assuming that the value of varname is value:

{:code;
    <div>{:;varname:}</div>
:}
{:code; {:flg; encode_tags :} >>
    <div>{:;varname:}</div>
:}

Output:

<div>value</div>
&lt;div&gt;value&lt;&#x2F;div&gt;

§Flag: encode_bifs

Encoding bifs, when “{:” and “:}” characters are encoded, the bifs will not be parsed, assuming that the value of varname is value:

{:code;
    <div>{:;varname:}</div>
:}
{:code; {:flg; encode_bifs :} >>
    <div>{:;varname:}</div>
:}

Output:

<div>value</div>
<div>&#123;:;varname:&#125;</div>

§Flag: safe

Encoding all, safe implies noparse, encode_tags and encode_bifs, assuming that the value of varname is value:

{:code;
    <div>{:;varname:}</div>
:}
{:code; {:flg; safe :} >>
    <div>{:;varname:}</div>
:}

Output:

<div>value</div>
&lt;div&gt;&#123;:;varname:&#125;&lt;&#x2F;div&gt;

§Flag: encode_tags_after

Encoding html tags after parsing, assuming that the content of “file.ntlp” is:

<div></div>

Then:

{:code; {:flg; encode_tags_after :} >>
    {:include; file.ntlp :}
:}

Output:

&lt;div&gt;&lt;&#x2F;div&gt;

§Nesting

Can be nested (no limit):

{:code;
    {:code;
        {:code;
            ...
        :}
    :}
:}

Grouping:

{:coalesce;
    {:code; {:* block 1 *:}
        {:code; ... :}
        {:code; ... :}
        {:code; ... :}
    :}
    {:code; {:* block 2 *:}
        {:code; ... :}
    :}
:}

§Params

If we need to create parameters they must necessarily reside in a “code” block:

{:code;
    {:param; ... :}
    {:param; ... :}
    ...
:}

See the bif “param” for more information on parameters.


§{:contains; … :}

Output code if contains.

{:contains; /literal/literal/ >> code :}
{:contains; /haystack/needle/ >> code :}
{:contains; /{:;varname:}/42/ >> ... :}

Any delimiter can be used:

{:contains; ~haystack~needle~ >> ... :}
{:contains; #haystack#needle# >> ... :}
{:contains; |haystack|needle| >> ... :}
{:contains; XhaystackXneedleX >> ... :}
...

§Modifiers:

{:^contains; ... :}
{:!contains; ... :}

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§Modifier: ! (not)

{:!contains; /haystack/needle/ >> this shown if is not contains. :}

§No flags


§{:count; … :}

DEPRECATED


§{:data; … :}

Set local data from json file.

{:data; local-data.json :}

The scope of the data is the file where it is loaded and its children.

To access the variables, the prefix “local::” must be used:

{:;local::varname:}

§Modifiers

{:!data; ... :}
{:^data; ... :}

The “not” modifier prevents the file from being reload if it has already been parsed.

{:!data; file.json :}

§Flags

{:data; {:flg; inline :} >> ... :}

§Flag: inline

{:data; {:flg; inline :} >>
    {
        "data": {
            "varname": "value"
        }
    }
:}

§Examples

Assumes local-data.json:

{
    "data": {
        "hello": "Hello!"
    }
}

Then:

{:data; local-data.json :}
{:;local::hello:}

Output:

Hello!

§{:date; … :}

Output UTC date

{:date;  :}

Default fotmat is timestamp

{:date; :}
{:date; %Y-%m-%d %H:%M:%S  :}

Output:

1729001985
2024-10-15 14:19:45

§Modifiers:

{:^date; :}

§No flags


§{:declare; … :}

Defines a list of allowed words separated by spaces for the bif “allow”, no output.

{:declare; name >> words list :}

Note that declare must be defined in a file whose name contains the word “snippet”. An error will occur if an attempt is made to define it elsewhere.

§Modifiers:

{:^declare; ... :}

§No flags

§Wildcards

  • (.) Dot, that matches any character.
  • (?) Question, that matches exactly one character.
  • (*) Asterisk, that matches zero or more characters.

§Use app data

Assumes:

{
    "data": {
        "themes": [
            "sketchy",
            "flatly",
            "darkly",
            "slate",
            "united"
        ]
    }
}

Then:

{:declare; themes >>
    {:each; themes key theme >>
        {:;:} {:;theme:}
    :}
:}

Unprintable {:;:} is necessary to preserve a space between words.

§Examples

{:declare; example >>
    en-??
    en_??
    en.US
    en?UK
:}
{:declare; languages >>
    en
    en-??
    en_??
    es
    es-??
    es_??
:}
{:declare; templates >>
    *.ntpl
:}
{:declare; colors >> red green blue :}
{:declare; any >> * :}

§{:defined; … :}

Output code if a variable is defined

{:defined; variable >> code :}

{:defined; varname >> this shown if varname is defined :}

§Modifiers:

{:^defined; ... :}
{:!defined; ... :}
{:+defined; ... :}

For more details about the “+” modifier see “modifiers”.

§Modifier: ! (not)

{:!defined; varname >> this shown if varname is not defined :}

§No flags

§Examples

{:defined; true          >> Shown! :}
{:defined; false         >> Shown! :}
{:defined; hello         >> Shown! :}
{:defined; zero          >> Shown! :}
{:defined; one           >> Shown! :}
{:defined; spaces        >> Shown! :}
{:defined; empty         >> Shown! :}
{:defined; null          >> Not shown :}
{:defined; undef         >> Not shown :}
{:defined; undefarr      >> Not shown :}
{:defined; emptyarr      >> Shown! :}
{:defined; array         >> Shown! :}
{:defined; array->true   >> Shown! :}
{:defined; array->false  >> Shown! :}
{:defined; array->hello  >> Shown! :}
{:defined; array->zero   >> Shown! :}
{:defined; array->one    >> Shown! :}
{:defined; array->spaces >> Shown! :}
{:defined; array->empty  >> Shown! :}
{:defined; array->null   >> Not shown :}
{:defined; array->undef  >> Not shown :}

Assumes data:

{
    "data": {
        "true": true,
        "false": false,
        "hello": "hello",
        "zero": "0",
        "one": "1",
        "spaces": "  ",
        "empty": "",
        "null": null,
        "emptyarr": [],
        "array": {
            "true": true,
            "false": false,
            "hello": "hello",
            "zero": "0",
            "one": "1",
            "spaces": "  ",
            "empty": "",
            "null": null
        }
    }
}

§{:each; … :}

Iterate array.

{:each; array keyname valuename >>
    code
:}

{:each; array-name key value >>
    {:;key:}={:;value:}
:}

§Modifiers:

{:^each; ... :}

§No flags

§Nesting

Can be nested (no limit), the following iterates an array, for more levels to increase the nesting:

{:^each; array key val >>
    {:array; val >>
        {:;:}
        {:;key:}:
        {:^each; val key val >>
            {:array; val >>
                {:;:}
                {:;key:}:
                {:^each; val key val >>
                    {:array; val >>
                        {:;:}
                        {:;key:}:
                        {:^each; val key val >>
                            {:;:}
                            {:;key:}={:;val:}
                        :}
                    :}{:else;
                        {:;:}
                        {:;key:}={:;val:}
                    :}
                :}
            :}{:else;
                {:;:}
                {:;key:}={:;val:}
            :}
        :}
    :}{:else;
        {:;:}
        {:;key:}={:;val:}
    :}
:}

For a recursive version of the above with no level limit, the following snippets could be set:

{:snippet; iterate-array >>
    {:^each; {:param; array-name :} key value >>
        {:array; value >>
            {:;:}
            {:;key:}:
            {:snippet; iterate-array-next-level :}
        :}{:else;
            {:;:}
            {:;key:}={:;value:}
        :}
    :}
:}

{:snippet; iterate-array-next-level >>
    {:^each; value key value >>
        {:array; value >>
            {:;:}<br>
            {:;key:}:<br>
            {:snippet; iterate-array-next-level :}
        :}{:else;
            {:;:}<br>
            {:;key:}={:;value:}
        :}
    :}
:}

§{:else; … :}

Output code if the output of the above bif called is empty (zero length).

{:else; code :}

{:;varname:}{:else; shown if varname is empty :}

§Modifiers:

{:^else; ... :}
{:!else; ... :}

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§Modifier: ! (not)

{:;varname:}{:!else; shown if varname is not empty :}

§No flags

§Nesting

Can be nested (no limit):

{:code; ... :}
{:else;
    {:;varname:}{:else; ... :}
:}

Grouping:

{:code;
    {:;foor:}
    {:;bar:}
:}{:else;
    foo and bar are empty
:}

Can be chained, the following is possible:

{:code;
    {:;foor:}
    {:;bar:}
:}{:else;
    foo and bar are empty
:}{:else;
    {:* if previous "else" is empty *:}
    foo or bar are not empty
:}

§Usage

Unpredictable results if not immediately after another bif, for example at the beginning of a template. Some bifs always return an empty string, so it doesn’t make much sense to add “else” after.

{:moveto; ... :}
{:else; moveto always outputs an empty string :}
{:param: ... :}
{:else; param always outputs an empty string :}

Regardless of the result of an expression, but the output of the block, in this example it does not matter if varname is defined, but if the block it returns is empty or not:

{:defined; varname >> {:* void *:} :}
{:else;
    The previous block is empty,
    but I don't know what happened to varname
:}

For a construction taking into account the result of the expression, you can do this other:

{:defined; varname >> ... :}
{:!defined; varname >> ... :}

Only the output of the last bif is relevant, it does not matter if there is something else in between. Next is the same:

{:code;
    {:;foor:}
    {:;bar:}
:}{:else;
    foo and bar are empty
:}
{:code;
    {:;foor:}
    {:;bar:}
:}
<div>
    ...
</div>
{:else;
    foo and bar are empty
:}

But the second way will be confusing.


§{:eval; … :}

Output code if the result of eval is not empty (non-zero length).

{:eval; code >> code :}

{:eval; {:;varname:} >>
    shown if varname is not empty
:}

A variable is set with the result of the evaluation:

{:eval; {:;varname:} >>
    ...
    {:;__eval__:}
    ...
:}

§Modifiers:

{:!eval; ... :}
{:+eval; ... :}
{:^eval; ... :}

For more details about the “+” modifier see “modifiers”.

§Modifier: ! (not)

{:!eval; {:;varname:} >>
    shown if varname is empty
:}

§No flags

§Usage

Eval evaluates code versus filled which evaluates a variable:

    {:filled; varname >>
        ...
    :}

    {:eval; {:code: {:;varname:} :} >>
        ...
    :}

Sometimes it is necessary to evaluate code:

<li>{:snippet; snippet-name :}</li>

The output of the above if “snippet-name” is empty will be:

<li></li>

To prevent this from happening, it can be done:

{:eval; {:snippet; snippet-name :} >>
    <li>{:;__eval__:}</li>
:}

And in this case the “li” will only be displayed if “snippet-name” is not empty.

The following are equivalent, but the first does not require re-evaluation of the snippet and may be clearer as to what is intended:

{:eval; {:snippet; snippet-name :} >>
    <li>{:;__eval__:}</li>
:}
{:eval; {:snippet; snippet-name :} >>
    <li>{:snippet; snippet-name :}</li>
:}

This is also possible

{:eval; {:;varname1:}{:;varname2:} >>
    ...
    {:;__eval__:}
    ...
:}
{:eval;
    {:code;
        ... a lot of code here ...
    :}
    >>
    ...
    {:;__eval__:}
    ...
:}

Can also be used to prevent output, in the following example, we want the include to show a possible output:

{:+eval; {:include; file :} >> :}

For more details about the “+” modifier see “modifiers”.


§{:exit; … :}

Terminate the current template with status code, 200 by default. No output.

{:exit; HTTP-STATUS-CODE :}

{:exit; :}

It is important to know that the status codes must be managed by the application, this bif is limited to set them and to terminate the template.

In addition, in status codes 400 to 599, 301, 302, 303, 307 and 308 the rendered content is removed and replaced by the status code.

§Modifiers:

{:!exit; ... :}

§Modifier: ! (not)

The “not” modifier prevents termination, continues executing the template and is limited only to setting a status code:

{:!exit; 202 :}

§No flags

§Some uses

In the following examples, the template is terminated and a status code is set:

{:exit; 401 :}
{:exit; 403 :}
{:exit; 404 :}
{:exit; 503 :}

A parameter is set here:

{:exit; 500 >> reason :}

§Redirect

With status codes 301, 302, 303, 307 and 308 you can create a redirect by adding the URL to the parameters:

{:exit; 301 >> /page :}
{:exit; 302 >> /page :}
{:exit; 303 >> /page :}
{:exit; 307 >> https://example.com :}
{:exit; 308 >> https://example.com :}

The “redirect” bif also performs this task.

§Custom status codes

Status codes from 100 to 999 are used or could be used in the future by the HTTP protocol, so it is better not to use them, but we could use a larger code to create our custom status codes.

For example, if we want to change the behavior of the 404 error where the content is removed when it is set, the status code “10404” could be used

{:exit; 10404 :}

Or:

{:exit; 10401 >> reason :}

And manage it in the application in another way.

§Manage in the app (native Rust)

If you use the bif “exit” or “redirect” it is necessary to manage the status codes in the application, it will depend on the environment or framework you are using, it could be something like this:

let template = Template::from_file_value("file.ntpl", schema).unwrap();
let content = template.render();

// e.g.: 500
let status_code = template.get_status_code();

// e.g.: Internal Server Error
let status_text = template.get_status_text();

// e.g.: template error x
let status_param = template.get_status_param();

// act accordingly at this point

It may be the case that the template contains a syntax error, in which case set the status code 500 and in the parameter the reason, by default the content is blank.

§Manage in the app (others)

Each programming language can implement it in a different way, see the IPC client of each language.

There will usually be an equivalent function with the same name as those in Rust.


§{:fetch; … :}

Perform a JS fetch request.

{:fetch; |url|event|wrapperId|class|id|name| >> code :}

Code is the html that will be displayed before performing the fetch, it can be a message, a button or the fields of a form.

  • url: url to perform fetch, mandatory.
  • event: can be; none, form, visible, click, auto, default auto.
  • wrapperId: alternative container wrapper ID, default none
  • class: add to container class, default none
  • id: container ID, default none
  • name: container name, default none

The url is the only mandatory parameter:

{:fetch; "/url" >> <div>loading...</div> :}

Any delimiter can be used, but a delimiter is always required, even if only one parameter is used.

{:fetch; "url" >> ... :}
{:fetch; ~url~event~ >> ... :}
{:fetch; #url#event# >> ... :}
{:fetch; |url|event| >> ... :}
{:fetch; 'url'event' >> ... :}
...

The reason you can use different delimiters is to use one that does not appear in the parameter, using / would cause problems with the url so we use " or any other:

-{:fetch; //url/ >> ... :} {:* error *:}
+{:fetch; "/url" >> ... :}

§Modifiers:

{:^fetch; ... :}

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§No flags

§event auto

Performs the fetch automatically on page load:

{:fetch; |/url|auto| >> <div>loading...</div> :}

§event none

No event, waiting for you to provide a custom event to perform the fetch:

{:fetch; |/url|none| >> ... :}

Do not confuse none with leaving the parameter empty, leaving it empty would be auto by default.

§event click

Perform the fetch on click:

{:fetch; |/url|click| >> <button type="button">Click Me!</button> :}

§event visible

Performs the fetch when the containing element is visible, it can be useful to display content in modals, images or on the scroll:

{:fetch; |/url|visible| >> <div>loading...</div> :}

§event form

The form event generates a form:

{:fetch; |/url|form| >>
    <input type="text" name="paramValue">
    <button type="submit">Send</button>
:}

The above will automatically generate HTML similar to this:

<form class="neutral-fetch-form" method="POST" action="/url">
    <input type="text" name="paramValue">
    <button type="submit">Send</button>
</form>

And the form will be sent with fetch formatting the parameters automatically, even if file uploads are included.

§neutral.js

By default when you first call fetch the necessary JavaScript script is automatically added to the end of body.

This behavior can be changed in schema with disable_js to true for performance reasons:

{
    "config": {
        "disable_js": true
    }
}

In this case you will have to manually add to your template at the end of body the script:

<html>
    <body>
        ...
        <script src="neutral.min.js"></script>
    </body>
</html>

You can download it here: neutral.min.js

§JS Variables

The following variables are available for the forms:

<script>
    var neutral_submit_loading = '...';
    var neutral_submit_timeout = 30000;
    var neutral_submit_error = '{:trans; Form ERROR! :}';
    var neutral_submit_error_delay = 3500;
    var neutral_submit_delay = 250;
</script>

An should be set before loading the neutral.js so <head> is a good place to do it:

  • neutral_submit_loading: It is added to the end of the text of the submit button.
  • neutral_submit_timeout: Timeout for form submission.
  • neutral_submit_error: Error message when sending the form.
  • neutral_submit_error_delay: Time it takes for the error message to disappear.
  • neutral_submit_delay: Delay for form submission, prevent double click.

§JS api

You can also use the Neutral TS JS api directly from JavaScript.

§Events

  • neutralFetchCompleted: Sent when a fetch request has been completed successfully.
  • neutralFetchError: Sent when a fetch request has produced an error.

The following parameters are available for events:

  • detail.element: element
  • detail.url: original url

Examples:

<script>
    window.addEventListener('neutralFetchCompleted', function(evt) {
        console.log(evt.detail.url);
        console.log(evt.detail.element);
    });
    window.addEventListener('neutralFetchError', function(evt) {
        console.log(evt.detail.url);
        console.log(evt.detail.element);
    });
</script>

§Class

  • neutral-fetch-form: Performs a fetch request from a form.
  • neutral-fetch-auto: Performs a fetch request when the page loads.
  • neutral-fetch-click: Performs a fetch request when click.
  • neutral-fetch-none: Does nothing.

§Functions

  • neutral_fev(): Reassign events for classes, may be necessary after fetch or xhr.
  • neutral_fetch_form(form, url, wrapper): Send a form by fetch.

§Example

The following example snippet to be placed in just above </body>.

{:snippet; neutral.js >>
    <script>

        // Spinner loading form
        var neutral_submit_loading = '{:snippet; spin-1x :}';

        // Timeout for send form
        var neutral_submit_timeout = 9000;

        // Translate error message
        var neutral_submit_error = '{:trans; Form ERROR! :}';

        // Time message remains in case of error
        var neutral_submit_error_delay = 5500;

        // Prevent double clicking
        var neutral_submit_delay = 300;

        // Execute the scripts contained in the fetch avoiding arbitrary code.
        // - Executes only it is not an external URL.
        // - Executes only those in a container with the script-container class.
        window.addEventListener('neutralFetchCompleted', function(evt) {
            if (!evt.detail.url.includes('://')) {
                const scriptContainer = evt.detail.element.querySelector('.script-container');
                if (scriptContainer) {
                    const scripts = scriptContainer.querySelectorAll('script');
                    scripts.forEach(script => {
                        const newScript = document.createElement('script');
                        newScript.text = script.textContent;
                        document.body.appendChild(newScript);
                    });
                }
            }
        });
    </script>
    <script src="/path/to/neutral.min.js"></script>
:}

Then:

<html>
    <head>
        ...
    </head>
    <body>
        ...
        {:snippet; neutral.js :}
    </body>
</html>

§{:filled; … :}

Output code if a variable is filled (non-zero length).

{:filled; variable >> code :}

{:filled; varname >> this shown if varname is filled :}

§Modifiers:

{:^filled; varname >> ... :}
{:!filled; varname >> ... :}
{:+filled; varname >> ... :}

For more details about the “+” modifier see “modifiers”.

§Modifier ! (not):

{:!filled; varname >> this shown if varname is not filled :}

§No flags

§Examples

{:filled; true          >> Shown! :}
{:filled; false         >> Shown! :}
{:filled; hello         >> Shown! :}
{:filled; zero          >> Shown! :}
{:filled; one           >> Shown! :}
{:filled; spaces        >> Shown! :}
{:filled; empty         >> Not shown :}
{:filled; null          >> Not shown :}
{:filled; undef         >> Not shown :}
{:filled; undefarr      >> Not shown :}
{:filled; emptyarr      >> Not shown :}
{:filled; array         >> Shown! :}
{:filled; array->true   >> Shown! :}
{:filled; array->false  >> Shown! :}
{:filled; array->hello  >> Shown! :}
{:filled; array->zero   >> Shown! :}
{:filled; array->one    >> Shown! :}
{:filled; array->spaces >> Shown! :}
{:filled; array->empty  >> Not shown :}
{:filled; array->null   >> Not shown :}
{:filled; array->undef  >> Not shown :}

Assumes data:

{
    "data": {
        "true": true,
        "false": false,
        "hello": "hello",
        "zero": "0",
        "one": "1",
        "spaces": "  ",
        "empty": "",
        "null": null,
        "emptyarr": [],
        "array": {
            "true": true,
            "false": false,
            "hello": "hello",
            "zero": "0",
            "one": "1",
            "spaces": "  ",
            "empty": "",
            "null": null
        }
    }
}

§{:flg; … :}

Set a flag for a bif.

{:flg; flag1 flag2 ... :}

It must be included in the parameters of a bif, by itself it has no utility.

Example:

{:include; {:flg; require :} >> ... :}

§{:for; … :}

For loop.

{:for; varname from..to >> code :}

{:for; var 1..10 >>
    {:;var:}
:}

§Modifiers:

{:^for; ... :}

§No flags

§Examples

{:for; n 1..10 >>
    {:;n:}
:}

Reverse:

{:for; n 10..1 >>
    {:;n:}
:}

§{:hash; … :}

Output a hash value (md5), a random value by default.

{:hash;  :}
{:hash; text :}

§Modifiers:

{:^hash; ... :}

§No flags

§Examples

{:hash; :}
{:hash; :}
{:hash; Hello World :}
{:hash; Hello World :}

Output

c341544b05307511ee7fd301a6842c9c
d981eb7814774d5b4a795484cee3005e
b10a8db164e0754105b7a99be72e3fe5
b10a8db164e0754105b7a99be72e3fe5

§{:include; … :}

Include and usually parse any file, usually a template.

{:include; filename :}

{:include; template.ntpl :}

§Modifiers:

{:!include; ... :}
{:^include; ... :}

§Modifier: ! (not)

The “not” modifier prevents the file from being reparse if it has already been parsed.

Assuming that the content of file.ntpl is simply the word “Hello”:

{:include; file.ntpl :}
...
{:include; file.ntpl :}

Output:

Hello
Hello

With “not”:

{:include; file.ntpl :}
...
{:!include; file.ntpl :}

Output:

Hello

The following produces the same result as above:

{:!include; file.ntpl :}
...
{:!include; file.ntpl :}

Or can the parse be forced:

{:!include; file.ntpl :}
...
{:include; file.ntpl :}

Output:

Hello
Hello

§Flags

{:include; {:flg; require noparse safe :} >> ... :}

§Flag: require

By default, no error will occur if the file to include does not exist, unless the “require” flag is set.

{:include; {:flg; require :} >> file.ntpl :}

§Flag: noparse

The “noparse” flag prevents the file to be included from being parsed.

{:include; {:flg; noparse :} >> file.css :}

§Flag: safe

Encoding all, safe implies noparse.

{:include; {:flg; safe :} >> file.txt :}

§Dynamic evaluation

The following will produce an error:

{:include; {:;varname:} :}

For safety reasons, when evaluating the complete variable it is necessary to use “allow”:

{:include; {:allow; allowed-words-list >> {:;varname:} :} :}
{:include; {:!allow; traversal >> {:;varname:} :} :}

In any case, you must use “allow” on any variable that comes from the context. See the “allow” and “declare” bifs for more details.

§Relative to current file path

When creating a web application, we’ll know where the files are located, so we can do:

{:include; /path/to/tpl/template.ntpl :}

We can use relative paths to the currently included file with the “#” symbol:

{:include; #/snippets.ntpl :}

When using “#” inside /path/to/tpl/template.ntpl, it becomes:

{:include; /path/to/tpl/snippets.ntpl :}

In this way, we can create utilities without knowing the directory structure.


§{:join; … :}

Output join array elements with a string.

{:join; /array/literal/bool/ :}
{:join; /array/separator/keys/ :}
{:join; /array/separator/ :}
  • array: an array
  • separator: string as a separator
  • keys: optional boolean for using keys instead of values, default false, values

Any delimiter can be used:

{:join; ~array~separator~ :}
{:join; #array#separator# :}
{:join; |array|separator| :}
{:join; XarrayXseparatorX :}

§Modifiers:

{:^join; ... :}

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§No flags

§Example

<li>{:join; |array|</li><li>| :}</li>

§{:locale; … :}

Includes a language json file:

{:locale; filename :}

{:locale; file.json :}

The language files must have a structure similar to this one:

{
    "trans": {
        "es": {
            "Hello": "Hola",
            "ref:greeting": "Hola",
        },
        "es-ES": {
            "Hello": "Hola",
            "ref:greeting": "Hola",
        }
    }
}

A “trans” key and then the key for each language, any other key will produce an error or be ignored.

§Modifiers:

{:!locale; ... :}
{:^locale; ... :}

§Modifier: ! (not)

The “not” modifier prevents the file from being reload if it has already been parsed. Generally, language files should only be included once, it will increase performance if “not” is used:

{:!locale; file.json :}

§Flags

{:locale; {:flg; require noparse :} >> ... :}

§Flag: require

By default, no error will occur if the file to locale does not exist, unless the “require” flag is set.

{:locale; {:flg; require :} >> file.json :}

§Flag: noparse

The “noparse” flag prevents the file to be included from being parsed.

{:locale; {:flg; noparse :} >> file.json :}

§Return value

“locale” show no output but returns “{:;:}” (unprintable) if successful, so this is possible:

{:locale; file.{:lang;:}.json :}
{:else; file.en.json :}

§Dynamic evaluation

The following will produce an error:

{:locale; {:;varname:} :}

For safety reasons, when evaluating the complete variable it is necessary to use “allow”:

{:locale; {:allow; allowed-words-list >> {:;varname:} :} :}
{:locale; {:!allow; traversal >> {:;varname:} :} :}

In any case, you must use “allow” on any variable that comes from the context. See the “allow” and “declare” bifs for more details.

§Translation evaluation

Translations may contain variables:

{
    "trans": {
        "es": {
            "Welcome to {:;web-site-name:}": "Bienvenido a {:;web-site-name:}"
        }
    }
}

Variables are resolved at the time the file is included.

(*) Translations included in the “schema” are not evaluated, only those loaded with “locale”.

§Relative to current file path

We can use relative paths to the currently localed file with the “#” symbol:

{:locale; #/file.json :}

It will be relative to the file loaded with “include”:

{:*
    file: /path/to/tpl/snippets.ntpl
    Here #/file.json is: /path/to/tpl/file.json
*:}

{:locale; #/file.json :}

§{:moveto; … :}

Move the code to the specified tag, no output. The code is parsed before moving.

{:moveto; tagname >> code :}

{:moveto; <tag >> ... :}
{:moveto; </tag >> ... :}

Moves to the inside of the tag, at the beginning or at the end depending on whether the slash is included.

Note that it is designed to move libraries, js, css, etc, not to arbitrarily move code to any tag, if it moves to a “div” it will do it to the first one it finds.

Custom tags can be created to mark the places where certain code should be moved.

§Modifiers:

{:^moveto; ... :}

§No flags

§Usage

An ID is generated with the code to be moved, so that the same code is only moved once. The following will only move once even if it is quoted several times throughout the app:

{:moveto; </head >> <script src="jquery.min.js"></script> :}
{:moveto; </head >> <script src="jquery.min.js"></script> :}

Since this is the desired behavior, so that the code is always the same, it is better to create snippets:

{:snippet; include-jquery >>
    <script src="jquery.min.js"></script>
:}

{:moveto; </head >> {:snippet; include-jquery :} :}

Or:

{:snippet; include-jquery >>
    {:moveto; </head >> <script src="jquery.min.js"></script> :}
:}

{:snippet; include-jquery :}

Example:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
    </head>
    <body>
        <script src="jquery.min.js"></script>
    </body>
</html>

{:moveto; </head >> <meta name="meta" content="anything"> :}
{:moveto; <body  >> <script> // script 1 </script> :} {:* move to start body *:}
{:moveto; </body >> <script> // script 2 </script> :} {:* move to end body *:}

Result:

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="meta" content="anything">
    </head>
    <body>
        <script> // script 1 </script>
        <script src="jquery.min.js"></script>
        <script> // script 2 </script>
    </body>
</html>

§{:neutral; … :}

Neutral returns to itself, no parse.

{:neutral; hello >> {:;world:} :}

Output:

{:neutral; hello >> {:;world:} :}

§Modifiers:

{:^neutral; ... :}

§No flags


§{:param; … :}

Set custom parameters, no output in set, output value in get.

{:param; name >> value :} {:* set *:}
{:param; name :}          {:* get *:}

§Modifiers:

{:^param; ... :}

§No flags

§Usage

The parameters must necessarily be defined within and at the beginning of the bif “code”.

{:code;
    {:param; name1 >> value :}
    {:param; name2 >> value :}
    ...
:}

The parameters have block scope, will be inherited and recover their value:

{:code;
    {:param; name >> 1 :}
    {:code;
        {:param; name >> 2 :}
    :}
    {:* "name" will recover their value *:}
:}

Example of passing arguments to template:

{:code;
    {:param; option >> value :}
    {:include; foo.tpl :}
:}

Example of passing arguments to snippet:

{:code;
    {:param; option >> blue :}
    {:snippet; foo :}
:}

The snippet may be:

{:snippet; foo >>
    The option is: {:param; option :}
:}

§{:rand; … :}

Output rand number.

{:rand; :}
{:rand; 1..10 :}

§Modifiers:

{:^rand; ... :}

§No flags

§Examples

{:rand; :}
{:rand; 1..10 :}
{:rand; 100..999 :}

Output:

688755410
3
569

§{:redirect; … :}

Terminate the current template with redirect status code or Javascript redirect.

{:redirect; 301 >> url :}
{:redirect; js:reload:top :}

It is important to know that the status codes must be managed by the application, this bif is limited to set them and to terminate the template. In the case of JS redirections, a status code “200 OK” is set and does not need to be managed in the app.

§Modifiers:

{:^redirect; ... :}

§No flags

§Redirect HTTP

The possible values for HTTP redirects are 301, 302, 303, 307 and 308 any other will generate an error. Redirect codes:

{
    "301": "Moved Permanently",
    "302": "Found",
    "303": "See Other",
    "307": "Temporary Redirect",
    "308": "Permanent Redirect"
}

{:redirect; 301 >> http://example.com :}
{:redirect; 302 >> http://example.com :}
{:redirect; 303 >> http://example.com :}
{:redirect; 307 >> http://example.com :}
{:redirect; 308 >> http://example.com :}

Custom redirections can also be created with the “exit” bif.

§Redirect Javascript

In this case it is not necessary to do anything in the app.

js:reload:top reloads the current page in the top window, no URL required:

{:redirect; js:reload:top :}

js:reload:self reloads the current page in the current window, no URL required:

{:redirect; js:reload:self :}

js:redirect:top redirects the page (URL) in the top window, requires the destination URL:

{:redirect; js:redirect;top >> /home/ :}

js:redirect:self redirects the page (URL) in the current window, requires the destination URL:

{:redirect; js:redirect;self >> /home/ :}

§Manage in the app (native Rust)

If you use the bif “exit” or “redirect” it is necessary to manage the status codes in the application, it will depend on the environment or framework you are using, it could be something like this:

let template = Template::from_file_value("file.ntpl", schema).unwrap();
let content = template.render();

// e.g.: 301
let status_code = template.get_status_code();

// e.g.: Moved Permanently
let status_text = template.get_status_text();

// e.g.: https://example.com
let status_param = template.get_status_param();

// act accordingly at this point

It may be the case that the template contains a syntax error, in which case set the status code 500 and in the parameter the reason, by default the content is blank.

§Manage in the app (others)

Each programming language can implement it in a different way, see the IPC client of each language.

There will usually be an equivalent function with the same name as those in Rust.


§{:replace; … :}

Replace all occurrences in code part.

{:replace; /from/to/ >> code :}

Any delimiter can be used:

{:replace; ~from~to~ >> ... :}
{:replace; ~\~/~ >> ... :}
{:replace; #from#to# >> ... :}
{:replace; |from|to| >> ... :}
{:replace; XfromXtoX >> ... :}
...

§Modifiers:

{:^replace; ... :}

§No flags

§Example

{:replace; / /_/ >> Hello World :}

Output

Hello_World

§{:same; … :}

Output code if same.

{:same; /literal/literal/ >> code :}

{:same; /{:;varname:}/42/ >> ... :}

Any delimiter can be used:

{:same; ~a~b~ >> ... :}
{:same; #a#b# >> ... :}
{:same; |a|b| >> ... :}
{:same; XaXbX >> ... :}
...

§{:snippet; … :}

Output or set snippet.

{:snippet; name >> ... :} {:* set  *:}
{:snippet; name :}        {:* play *:}

You can use the alias “snip”, this is the same:

{:snip; name >> ... :} {:* set  *:}
{:snip; name :}        {:* play *:}

Note that snippet, although they can be called anywhere, must be defined in a file whose name contains the word “snippet”. An error will occur if an attempt is made to define it elsewhere.

§Modifiers:

{:^snippet; ... :}

§Flags

{:snippet; {:flg; static :} ... >> ... :}

§Flag: static

By default the content of the snippet is parsed when called, with “static” it is parsed when set.

{:snippet; {:flg; static :} snippet-name >>
    {:;varname:}
:}

Therefore, it only works on set.

§Usage

The snippet is something like a function, parts of code that you can refer and can contain anything.

Set snippet, no output:

{:snippet; snippet-name >>
    Hello World
:}

Play snippet;

{:snippet; snippet-name :}

Output:

Hello World

§{:sum; … :}

Output the sum.

{:sum; /literal/literal/ :}

Any delimiter can be used:

{:sum; ~a~b~ :}
{:sum; #a#b# :}
{:sum; |a|b| :}
{:sum; XaXbX :}
...

§Modifiers:

{:^sum; ... :}

§Modifier: ^ (upline)

Eliminates previous whitespaces, (See “unprintable” for examples.)

§No flags


§{:trans; … :}

Translate a string.

{:trans; String to translate :}

§Modifiers

{:^trans; ... :}
{:!trans; ... :}
{:^!trans; ... :}

§Modifier: ^ (upline)

Eliminates previous whitespaces

Assuming that the translation of Hello is Hello:

<div></div>

{:trans; Hello :}

<div></div>

{:^trans; Hello :}

Output:

<div></div>

Hello

<div></div>Hello

§Modifier: ! (not)

By default the same string is output if there is no translation, to output an empty string use the modifier “not”. Assuming there is no translation for “Hello”.

<div>{:trans; Hello :}</div>
<div>{:!trans; Hello :}</div>
<div>{:!trans; Hello :}{:else;
    There is no translation for "Hello".
:}</div>

Output:

<div>Hello</div>
<div></div>
<div>There is no translation for "Hello".</div>

§No flags

§References

An APP can be created thinking that in the future it can be translated, we use in all the “trans” outputs;

<ul>
    <li>{:trans; File :}</li>
    <li>{:trans; Edit :}</li>
</ul>

Simply add the translation without modifying the code.

We can also add a translation from the beginning and use the same or another strategy as references:

<ul>
    <li>{:trans; ref:menu:file :}</li>
    <li>{:trans; ref:menu:edit :}</li>
</ul>

<ul>
    <li>{:trans; code:1002 :}</li>
    <li>{:trans; code:1003 :}</li>
</ul>

In this case it will be necessary to make sure that all references have a translation, or use “not” and “else”:

<li>{:!trans; ref:menu:edit :}{:else; Edit :}</li>

The “schema” takes care of the translation by means of the “locale” key of the scheme, the current language is defined with the “current” key:

{
    "inherit": {
        "locale": {
            "current": "en"
            "trans": {
                "en": {
                    "Hello": "Hello",
                    "ref:greeting": "Hello"
                },
                "es": {
                    "Hello": "Hola",
                    "ref:greeting": "Hola"
                },
                "de": {
                    "Hello": "Hallo",
                    "ref:greeting": "Hallo"
                },
                "fr": {
                    "Hello": "Bonjour",
                    "ref:greeting": "Bonjour"
                }
            }
        }
    }
}

For more details see “schema” and “locale” bif.

§Non-text translation

The above strategies will work well for translating short text strings, but not for long text or even images.

We can use references to translate images:

<img src="/images/cover-{:!trans; ref:lang :}{:else; english :}.jpg" />

For which we will need images with the appropriate file name:

cover-english.jpg
cover-spanish.jpg
cover-french.jpg

With “else” we have set the default image, in case there is no translated image. Alternatively, we can do the following:

<img src="/images/cover-{:!trans; ref:lang :}.jpg" />

cover-.jpg          // This is the default image
cover-english.jpg
cover-spanish.jpg
cover-french.jpg

And the corresponding translations in “locale”:

{
    "locale": {
        "trans": {
            "en": {
                "ref:lang": "english"
            },
            "en-us": {
                "ref:lang": "english"
            },
            "es": {
                "ref:lang": "spanish"
            },
            "es-es": {
                "ref:lang": "spanish"
            },
            "fr": {
                "ref:lang": "french"
            }
        }
    }
}

This same system will be useful for translating long texts using the same defined references:

{:snippet; terms-conditions-{:!trans; ref:lang :}{:else; english :} :}
{:include; terms-conditions-{:!trans; ref:lang :}{:else; english :}.ntpl :}

Another strategy for translating images, long text and other content is the use of the “lang” bif which outputs the current language, first we define the snippet needed for each language, which can contain text, images, html, …:

{:snippet; contents-for-en >>
    ....
:}

{:snippet; contents-for-es >>
    ....
:}

{:snippet; contents-for-fr >>
    ....
:}

Then:

{:snippet; contents-for-{:lang;:} :}{:else;
    {:snippet; contents-for-en :}
:}

A snippet that does not exist or is empty is not an error, but “else” detects it.