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.
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();
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).
.-- 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 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
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;
...
:}
:}
:}
{:coalesce;
{:code; {:* block 1 *:}
{:code; ... :}
{:code; ... :}
{:code; ... :}
:}
{:code; {:* block 2 *:}
{:code; ... :}
:}
:}
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>
:}
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 :} :}
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.
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>{:exit;:}</div>
<div>{:exit;:}</div>
<div>{:exit;:}</div>
<div>{:exit;:}</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:} :}
:}
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>
{:;inject:}
<div>{:exit;:}</div>
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
:}
:}
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:
& → &
< → <
> → >
" → "
' → '
/ → /
{ → {
} → }
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.
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.
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
}
}
}
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();
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 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 >> ... :}
Eliminates previous whitespaces.
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 *:}
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 *:}
Escapes special HTML characters and braces:
& → &
< → <
> → >
" → "
' → '
/ → /
{ → {
} → }
{:* 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.
Output var value.
{:;varname:}
{:;array->key:}
{:^;varname:}
{:&;varname:}
{:!;varname:}
Assuming that the value of “varname” is “value”:
<div></div>
{:;varname:}
<div></div>
{:^;varname:}
Output:
<div></div>
value
<div></div>value
Escapes special HTML characters and braces:
& → &
< → <
> → >
" → "
' → '
/ → /
{ → {
} → }
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.
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:}
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:}
{:;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.
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>
Output empty string, eliminates or preserves spaces.
{:;:}
{:^;:}
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
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>
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 :}
:}
:}
{:!allow; ... :}
{:^allow; ... :}
Eliminates previous whitespaces, (See “unprintable” for examples.)
Output of an empty string in case it is found in the “declare”.
{:allow; {:flg; partial casein replace :} name >> ... :}
It would be the equivalent of having wildcards in the word list, from “word” to “word”.
Case insensitive
Returns the word found (without wildcards) instead of the evaluation text.
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.
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>
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.
{:!array; ... :}
{:+array; ... :}
{:^array; ... :}
For more details about the “+” modifier see “modifiers”.
Eliminates previous whitespaces, (See “unprintable” for examples.)
{:!array; varname >> this shown if varname is not array :}
{: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
}
}
}
Output code if a variable is true
{:bool; variable >> code :}
{:bool; varname >> this shown if varname is true :}
{:!bool; varname >> ... :}
{:^bool; varname >> ... :}
Eliminates previous whitespaces, (See “unprintable” for examples.)
{:!bool; varname >> this shown if varname is false :}
{: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
}
}
}
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 *:}
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| >> ... :}
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 :}
:}
{:^cache; ... :}
{:!cache; ... :}
Eliminates previous whitespaces, (See “unprintable” for examples.)
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>
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": {
"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:
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.
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.
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.
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:}
:}
{:+coalesce; varname >> ... :}
{:^coalesce; varname >> ... :}
For more details about the “+” modifier see “modifiers”.
Eliminates previous whitespaces, (See “unprintable” for examples.)
Can be nested (no limit):
{:coalesce;
{:coalesce;
{:code; ... :}
{:code; ... :}
{:code; ... :}
:}
{:code;
{:code; ... :}
:}
:}
Each block can be a single Bif, or a nested set of them:
{:coalesce;
.---- {:code;
| {:code; ... :}
Block --> | {:code; ... :}
| {:code; ... :}
`---- :}
.---- {:code;
Block --> | {:code; ... :}
`---- :}
Block --------> {:;varname:}
:}
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
:}
:}
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 :}
{:+code; ... :}
{:^code; ... :}
For more details about the “+” modifier see “modifiers”.
Eliminates previous whitespaces, (See “unprintable” for examples.)
{:code; {:flg; safe noparse encode_tags encode_bifs encode_tags_after :} >> ... :}
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>
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>
<div>value</div>
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>{:;varname:}</div>
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>
<div>{:;varname:}</div>
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:
<div></div>
Can be nested (no limit):
{:code;
{:code;
{:code;
...
:}
:}
:}
Grouping:
{:coalesce;
{:code; {:* block 1 *:}
{:code; ... :}
{:code; ... :}
{:code; ... :}
:}
{:code; {:* block 2 *:}
{:code; ... :}
:}
:}
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.
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 >> ... :}
...
{:^contains; ... :}
{:!contains; ... :}
Eliminates previous whitespaces, (See “unprintable” for examples.)
{:!contains; /haystack/needle/ >> this shown if is not contains. :}
DEPRECATED
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:}
{:!data; ... :}
{:^data; ... :}
The “not” modifier prevents the file from being reload if it has already been parsed.
{:!data; file.json :}
{:data; {:flg; inline :} >> ... :}
{:data; {:flg; inline :} >>
{
"data": {
"varname": "value"
}
}
:}
Assumes local-data.json:
{
"data": {
"hello": "Hello!"
}
}
Then:
{:data; local-data.json :}
{:;local::hello:}
Output:
Hello!
Output UTC date
{:date; :}
Default fotmat is timestamp
{:date; :}
{:date; %Y-%m-%d %H:%M:%S :}
Output:
1729001985
2024-10-15 14:19:45
{:^date; :}
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.
{:^declare; ... :}
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.
{: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 >> * :}
Output code if a variable is defined
{:defined; variable >> code :}
{:defined; varname >> this shown if varname is defined :}
{:^defined; ... :}
{:!defined; ... :}
{:+defined; ... :}
For more details about the “+” modifier see “modifiers”.
{:!defined; varname >> this shown if varname is not defined :}
{: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
}
}
}
Iterate array.
{:each; array keyname valuename >>
code
:}
{:each; array-name key value >>
{:;key:}={:;value:}
:}
{:^each; ... :}
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:}
:}
:}
:}
Output code if the output of the above bif called is empty (zero length).
{:else; code :}
{:;varname:}{:else; shown if varname is empty :}
{:^else; ... :}
{:!else; ... :}
Eliminates previous whitespaces, (See “unprintable” for examples.)
{:;varname:}{:!else; shown if varname is not empty :}
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
:}
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.
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__:}
...
:}
{:!eval; ... :}
{:+eval; ... :}
{:^eval; ... :}
For more details about the “+” modifier see “modifiers”.
{:!eval; {:;varname:} >>
shown if varname is empty
:}
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”.
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.
{:!exit; ... :}
The “not” modifier prevents termination, continues executing the template and is limited only to setting a status code:
{:!exit; 202 :}
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 :}
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.
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.
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.
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.
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.
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" >> ... :}
{:^fetch; ... :}
Eliminates previous whitespaces, (See “unprintable” for examples.)
Performs the fetch
automatically on page load:
{:fetch; |/url|auto| >> <div>loading...</div> :}
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.
Perform the fetch
on click:
{:fetch; |/url|click| >> <button type="button">Click Me!</button> :}
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> :}
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.
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
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:
You can also use the Neutral TS
JS api directly from JavaScript.
The following parameters are available for events:
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>
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>
Output code if a variable is filled (non-zero length).
{:filled; variable >> code :}
{:filled; varname >> this shown if varname is filled :}
{:^filled; varname >> ... :}
{:!filled; varname >> ... :}
{:+filled; varname >> ... :}
For more details about the “+” modifier see “modifiers”.
{:!filled; varname >> this shown if varname is not filled :}
{: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
}
}
}
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 loop.
{:for; varname from..to >> code :}
{:for; var 1..10 >>
{:;var:}
:}
{:^for; ... :}
{:for; n 1..10 >>
{:;n:}
:}
Reverse:
{:for; n 10..1 >>
{:;n:}
:}
Output a hash value (md5), a random value by default.
{:hash; :}
{:hash; text :}
{:^hash; ... :}
{:hash; :}
{:hash; :}
{:hash; Hello World :}
{:hash; Hello World :}
Output
c341544b05307511ee7fd301a6842c9c
d981eb7814774d5b4a795484cee3005e
b10a8db164e0754105b7a99be72e3fe5
b10a8db164e0754105b7a99be72e3fe5
Include and usually parse any file, usually a template.
{:include; filename :}
{:include; template.ntpl :}
{:!include; ... :}
{:^include; ... :}
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
{:include; {:flg; require noparse safe :} >> ... :}
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 :}
The “noparse” flag prevents the file to be included from being parsed.
{:include; {:flg; noparse :} >> file.css :}
Encoding all, safe implies noparse.
{:include; {:flg; safe :} >> file.txt :}
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.
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.
Output join array elements with a string.
{:join; /array/literal/bool/ :}
{:join; /array/separator/keys/ :}
{:join; /array/separator/ :}
Any delimiter can be used:
{:join; ~array~separator~ :}
{:join; #array#separator# :}
{:join; |array|separator| :}
{:join; XarrayXseparatorX :}
{:^join; ... :}
Eliminates previous whitespaces, (See “unprintable” for examples.)
<li>{:join; |array|</li><li>| :}</li>
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.
{:!locale; ... :}
{:^locale; ... :}
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 :}
{:locale; {:flg; require noparse :} >> ... :}
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 :}
The “noparse” flag prevents the file to be included from being parsed.
{:locale; {:flg; noparse :} >> file.json :}
“locale” show no output but returns “{:;:}” (unprintable) if successful, so this is possible:
{:locale; file.{:lang;:}.json :}
{:else; file.en.json :}
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.
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”.
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 :}
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.
{:^moveto; ... :}
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 returns to itself, no parse.
{:neutral; hello >> {:;world:} :}
Output:
{:neutral; hello >> {:;world:} :}
{:^neutral; ... :}
Set custom parameters, no output in set, output value in get.
{:param; name >> value :} {:* set *:}
{:param; name :} {:* get *:}
{:^param; ... :}
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 :}
:}
Output rand number.
{:rand; :}
{:rand; 1..10 :}
{:^rand; ... :}
{:rand; :}
{:rand; 1..10 :}
{:rand; 100..999 :}
Output:
688755410
3
569
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.
{:^redirect; ... :}
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.
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/ :}
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.
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 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 >> ... :}
...
{:^replace; ... :}
{:replace; / /_/ >> Hello World :}
Output
Hello_World
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 >> ... :}
...
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.
{:^snippet; ... :}
{:snippet; {:flg; 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.
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
Output the sum.
{:sum; /literal/literal/ :}
Any delimiter can be used:
{:sum; ~a~b~ :}
{:sum; #a#b# :}
{:sum; |a|b| :}
{:sum; XaXbX :}
...
{:^sum; ... :}
Eliminates previous whitespaces, (See “unprintable” for examples.)
Translate a string.
{:trans; String to translate :}
{:^trans; ... :}
{:!trans; ... :}
{:^!trans; ... :}
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
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>
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.
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.