Neutral Template System is a safe, modular, language-agnostic template engine built in Rust. It can be used as a native Rust library or via IPC for other languages (Python, PHP, Node.js, Go). Templates can be reused across multiple languages with consistent results.
This is a summary for the context of AI; the full documentation is here: Neutral TS Doc
{:*
comment
*:}
{:locale; locale.json :}
{:data; local-data.json :}
{:include; theme-snippets.ntpl :}
<!DOCTYPE html>
<html lang="{:lang;:}">
<head>
<title>{:trans; Site title :}</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
{:snip; current-theme:head :}
<link rel="stylesheet" href="bootstrap.min.css">
</head>
<body class="{:;body-class:}">
{:snip; current-theme:body_begin :}
{:snip; current-theme:body-content :}
{:snip; current-theme:body-footer :}
<script src="jquery.min.js"></script>
</body>
</html>
File Extension: .ntpl (Neutral Template)
Core Philosophy:
All NTPL functionality uses BIFs with this exact syntax:
{:[modifiers]name; [flags] params >> code :}
Anatomy:
{:[modifiers]name; [flags] params >> code :}
│ │ │ │ │ │ │
│ │ │ │ │ │ └─ Close BIF (always :})
│ │ │ │ │ └─ Code block (content to render)
│ │ │ │ └─ Params/Code separator (>>)
│ │ │ └─ Parameters (variables, files, conditions)
│ │ └─ Name separator (;)
│ └─ BIF name (snippet, trans, include, etc.)
└─ Open BIF ({:)
Modifiers (prefix before name):
| Modifier | Symbol | Purpose |
|———-|——–|———|
| Upline | ^ | Eliminates preceding whitespace |
| Not | ! | Negates condition or changes behavior |
| Scope | + | Promotes definition to parent/current scope |
| Filter | & | Escapes HTML special characters |
Example with modifiers:
{:^!filled; varname >> content :} {:* No whitespace, negated, filtered *:}
Syntax:
{:* Single line comment *:}
{:*
Multi-line comment
------------------
Can span multiple lines
*:}
{:* Inline comment in BIF :}
{:;varname {:* get user name *:} :}
{:*
Nested comments are supported
{:* Inner comment *:}
{:*
Multiple levels
{:* Deep nesting *:}
*:}
*:}
schema.data)Accessed via {:;varname:} - set by Python/backend, cannot be overridden by templates.
{:;site_name:} {:* Simple variable *:}
{:;user->email:} {:* Object property *:}
{:;array->{:;index:}:} {:* Dynamic array access *:}
schema.inherit.data)Accessed via {:;local::varname:} - can be overridden by templates at runtime.
{:;local::current->route->title:}
{:;local::user->name:}
All user-provided data (GET, POST, COOKIES, ENV) is auto-escaped and stored in CONTEXT:
{:;CONTEXT->POST->username:} {:* Auto-escaped POST data *:}
{:;CONTEXT->GET->id:} {:* Auto-escaped GET parameter *:}
{:;CONTEXT->SESSION->userId:} {:* Session data *:}
{:;CONTEXT->HEADERS->User-Agent:} {:* Request headers *:}
Security Rule: Never use {:!;CONTEXT->...:} (unfiltered) unless absolutely necessary.
| BIF | Syntax | Purpose |
|---|---|---|
| Variable | {:;varname:} |
Output immutable data |
| Local Variable | {:;local::varname:} |
Output mutable local data |
| Unprintable | {:;:} or {:^;:} |
Output empty string (whitespace control) |
| Filtered Output | {:&;varname:} |
HTML-escaped output |
Unprintable BIF details:
{:* Basic unprintable - preserves spaces :}
{:;:}
{:* Upline unprintable - eliminates preceding whitespace :}
{:^;:}
{:* Usage examples :}
|<pre>
{:code;
{:^;:} {:* Eliminates leading newline *:}
Hello
{:^;:} {:* Eliminates trailing newline *:}
:}
</pre>|
{:* Check if variable has content (non-empty, non-null, defined) *:}
{:filled; varname >>
<p>Has content: {:;varname:}</p>
:}
{:* Negated - check if empty or undefined *:}
{:!filled; varname >>
<p>Variable is empty or undefined</p>
:}
{:* Check if variable is defined (exists, even if null/empty) *:}
{:defined; varname >>
<p>Variable exists</p>
:}
{:* Check if NOT defined *:}
{:!defined; varname >>
<p>Variable does not exist</p>
:}
{:* Boolean check - true values: true, non-zero, non-empty strings *:}
{:bool; varname >>
<p>Truthy value</p>
:}
{:* Boolean negated *:}
{:!bool; varname >>
<p>Falsy value</p>
:}
{:* String comparison - any delimiter works :}
{:same; /{:;status:}/active/ >>
<p>Status is active</p>
:}
{:* Negated comparison *:}
{:!same; /{:;status:}/active/ >>
<p>Status is not active</p>
:}
{:* Substring check *:}
{:contains; /{:;text:}/search/ >>
<p>Contains "search"</p>
:}
{:* Negated contains *:}
{:!contains; /{:;text:}/search/ >>
<p>Does not contain "search"</p>
:}
{:* Array check *:}
{:array; varname >>
<p>Is an array/object</p>
:}
{:* Negated array check *:}
{:!array; varname >>
<p>Is not an array</p>
:}
{:* Use else *:}
{:filled; varname >>
<p>Has content: {:;varname:}</p>
:}{:else;
<p>Variable is empty or undefined</p>
:}
Else blocks (evaluate whether the PREVIOUS BIF produced output):
{:* Else evaluates if previous BIF output was empty *:}
{:filled; varname >> <p>Has content</p> :}
{:else; <p>Is empty</p> :}
{:* Chain multiple else blocks *:}
{:code;
{:;foo:}
{:;bar:}
:}{:else;
foo and bar are empty
:}{:else;
{:* This fires if previous else was empty (i.e., foo or bar had content) *:}
foo or bar has content
:}
Truth Table (from documentation): | Variable | Value | filled | defined | bool | array | |———-|——-|——–|———|——|——-| | true | true | ✅ | ✅ | ✅ | ❌ | | false | false | ✅ | ✅ | ❌ | ❌ | | “hello” | string | ✅ | ✅ | ✅ | ❌ | | “0” | string | ✅ | ✅ | ❌ | ❌ | | “1” | string | ✅ | ✅ | ✅ | ❌ | | “ “ | spaces | ✅ | ✅ | ✅ | ❌ | | “” | empty | ❌ | ✅ | ❌ | ❌ | | null | null | ❌ | ❌ | ❌ | ❌ | | undef | — | ❌ | ❌ | ❌ | ❌ | | [] | empty arr | ❌ | ✅ | ❌ | ✅ | | {…} | object | ✅ | ✅ | ✅ | ✅ |
{:* Loop through array/object - each *:}
{:each; users key user >>
<div class="user">
<span>{:;key:}</span>
<span>{:;user->name:}</span>
<span>{:;user->email:}</span>
</div>
:}
{:* With upline modifier for clean output :}
{:^each; items idx item >>
<li>{:;idx:}: {:;item:}</li>
:}
{:* Numeric loop - for *:}
{:for; i 1..10 >>
<span>{:;i:}</span>
:}
{:* Reverse loop *:}
{:for; i 10..1 >>
<span>{:;i:}</span>
:}
{:* Nested iteration example *:}
{:^each; array key val >>
{:array; val >>
{:;:}
{:;key:}:
{:^each; val key val >>
{:array; val >>
{:;:}
{:;key:}:
{:^each; val key val >>
{:;:}
{:;key:}={:;val:}
:}
:}{:else;
{:;:}
{:;key:}={:;val:}
:}
:}
:}{:else;
{:;:}
{:;key:}={:;val:}
:}
:}
Snippets are reusable code fragments in templates, similar to functions, that avoid repetition by allowing you to define a block (such as a form) once and use it across multiple templates. You can use {:snip; or {:snip; interchangeably.
{:* Define a snippet (must be in file with "snippet" in name) *:}
{:snip; user-card >>
<div class="card">
<h3>{:;local::user->name:}</h3>
<p>{:;local::user->bio:}</p>
</div>
:}
{:* Use the snippet *:}
{:snip; user-card :}
{:* Alternative syntax *:}
{:snip; user-card :}
{:* Snippet with static flag (parsed at definition time) *:}
{:snip; {:flg; static :} user-card-static >>
{:;varname:} {:* Evaluated when DEFINED, not when called *:}
:}
{:* Scope modifiers with snippets - + promotes to parent scope *:}
{:+code;
{:include; snippet.ntpl :}
{:snip; name :} {:* Available outside due to + *:}
:}
{:snip; name :} {:* Works because of + above *:}
{:* Basic include *:}
{:include; header.ntpl :}
{:* Required include (errors if missing) *:}
{:include; {:flg; require :} >> config.ntpl :}
{:* Include without parsing (raw content) *:}
{:include; {:flg; noparse :} >> styles.css :}
{:* Safe include (encoded, no parsing) *:}
{:include; {:flg; safe :} >> readme.txt :}
{:* Relative to current file - # symbol *:}
{:include; #/snippets.ntpl :} {:* Same directory *:}
{:include; #/../shared/nav.ntpl :} {:* Parent directory *:}
{:* Dynamic include with allowlist (SECURITY CRITICAL) *:}
{:declare; valid-pages >>
home.ntpl
about.ntpl
contact.ntpl
:}
{:include;
{:allow; valid-pages >> {:;page:} :}
{:else; error.ntpl :}
:}
{:* Prevent reparse with ! modifier *:}
{:!include; already-parsed.ntpl :}
{:* Basic translation *:}
{:trans; Hello World :}
{:* Translation with fallback to empty *:}
{:!trans; Hello World :}{:else; Default text :}
{:* Reference-based translation *:}
{:trans; ref:menu:home :}
{:* Load locale file *:}
{:locale; locale-en.json :}
{:locale; {:flg; require :} >> #/locale-{:lang;:}.json :}
{:* Prevent reload if already loaded *:}
{:!locale; locale-es.json :}
{:* Upline modifier for clean whitespace *:}
{:^trans; Hello :}
Locale file structure (locale-es.json):
{
"__comment_:trans": "This translation will be available for the entire Component",
"trans": {
"es": {
"Hello World": "Hola Mundo",
"ref:menu:home": "Inicio",
"Welcome to {:;site-name:}": "Bienvenido a {:;site-name:}"
}
}
}
{:* Load local data from JSON *:}
{:data; local-data.json :}
{:* Inline data *:}
{:data; {:flg; inline :} >>
{
"data": {
"items": ["one", "two", "three"]
}
}
:}
{:* Prevent reload *:}
{:!data; already-loaded.json :}
{:* Access loaded data via local:: *:}
{:;local::items->0:}
{:* Cache block for 300 seconds *:}
{:cache; /300/ >>
<div>{:;expensive-content:}</div>
:}
{:* Cache with custom ID added to auto-generated ID *:}
{:cache; /300/my-custom-id/ >>
<div>Content</div>
:}
{:* Replace auto-generated ID completely (third param = 1/true) *:}
{:cache; /300/my-id/1/ >>
<div>Content</div>
:}
{:* Exclude from cache (always fresh) *:}
{:!cache;
<div>{:date; %H:%M:%S :}</div> {:* Current time, not cached *:}
:}
{:* Nested caching *:}
{:cache; /60/ >>
Outer cached content (60s)
{:cache; /300/ >>
Inner cached longer (300s)
:}
{:!cache;
Always fresh (not cached)
:}
:}
{:* Upline modifier *:}
{:^cache; /60/ >> content :}
{:* Set parameters within code block *:}
{:code;
{:param; title >> Dashboard :}
{:param; user_count >> 42 :}
<h1>{:param; title :}</h1>
<span>{:param; user_count :} users</span>
:}
{:* Parameters have block scope and recover their value *:}
{:code;
{:param; name >> 1 :}
{:code;
{:param; name >> 2 :}
:}
{:* "name" recovers value 1 here *:}
{:param; name :} {:* Outputs 1 *:}
:}
{:* Auto-fetch on page load (default) *:}
{:fetch; |/api/data|auto| >>
<div class="loading">Loading...</div>
:}
{:* Or simply (auto is default) *:}
{:fetch; "/api/data" >> <div>Loading...</div> :}
{:* Click-triggered fetch *:}
{:fetch; |/api/action|click| >>
<button>Load Data</button>
:}
{:* Visible-triggered fetch (intersection observer) *:}
{:fetch; |/api/lazy|visible| >>
<div class="placeholder">Scroll to load</div>
:}
{:* Form submission via fetch *:}
{:fetch; |/api/submit|form|form-wrapper|my-form-class|my-form-id| >>
<input type="text" name="username">
<button type="submit">Submit</button>
:}
{:* With wrapper ID for target container *:}
{:fetch; |/api/content|auto|content-wrapper| >>
<div>Loading content...</div>
:}
{:* No event - manual trigger *:}
{:fetch; |/api/manual|none| >> <div>Manual load</div> :}
{:* Upline modifier *:}
{:^fetch; |/url|auto| >> content :}
HTTP Header: All fetch requests set requested-with-ajax: fetch
{:* Basic code block (no action, just grouping) *:}
{:code;
<div>Any content</div>
{:;variable:}
:}
{:* Safe mode - encode everything *:}
{:code; {:flg; safe :} >>
<div>{:;untrusted:}</div> {:* Will be encoded *:}
:}
{:* No parse - output raw BIF syntax *:}
{:code; {:flg; noparse :} >>
<div>{:;not-parsed:}</div> {:* Shows literal {:;not-parsed:} *:}
:}
{:* Encode HTML tags *:}
{:code; {:flg; encode_tags :} >>
<div>HTML tags encoded</div>
:}
{:* Encode BIFs *:}
{:code; {:flg; encode_bifs :} >>
<div>{:;bif:}</div> {:* Shows {:;bif:} *:}
:}
{:* Encode tags after parsing *:}
{:code; {:flg; encode_tags_after :} >>
{:include; file.ntpl :} {:* Parsed, then encoded *:}
:}
{:* Upline modifier *:}
{:^code; content :}
{:* Scope modifier *:}
{:+code; content :}
{:* Coalesce - first non-empty value *:}
{:coalesce;
{:;option1:}
{:;option2:}
{:code; Default value :}
:}
{:* Evaluate and capture result *:}
{:eval; {:;complex-expression:} >>
Result: {:;__eval__:}
:}
{:* Negated eval *:}
{:!eval; {:;empty-var:} >>
Shown if empty
:}
{:* Exit with status code *:}
{:exit; 404 :} {:* Stop, 404 error, content cleared *:}
{:!exit; 302 :} {:* Set 302, continue execution *:}
{:exit; 301 >> /new-url :} {:* Redirect to URL *:}
{:* Custom status codes (1000+ range for app-specific) *:}
{:exit; 10404 >> not-found :} {:* Custom 404-like code *:}
{:* Redirect BIF *:}
{:redirect; 301 >> /destination :}
{:redirect; js:reload:top :} {:* JS reload top window *:}
{:redirect; js:reload:self :} {:* JS reload current frame *:}
{:redirect; js:redirect;top >> /url :} {:* JS redirect top *:}
{:redirect; js:redirect;self >> /url :} {:* JS redirect self *:}
{:* Upline modifiers *:}
{:^coalesce; ... :}
{:^eval; ... :}
{:^exit; ... :}
{:^redirect; ... :}
{:* Declare allowlist - MUST be in snippet file *:}
{:declare; allowed-templates >>
home.ntpl
about.ntpl
*.ntpl
:}
{:* Declare with wildcards *:}
{:declare; languages >>
en
en-??
en_??
es
es-??
es_??
:}
{:* Check against allowlist *:}
{:allow; allowed-templates >> {:;template:} :}
{:* Negated - check if NOT in list *:}
{:!allow; blocked-items >> {:;item:} :}
{:* With flags *:}
{:allow; {:flg; partial :} patterns >> {:;value:} :} {:* Wildcard matching *:}
{:allow; {:flg; casein :} patterns >> {:;value:} :} {:* Case-insensitive *:}
{:allow; {:flg; replace :} patterns >> {:;value:} :} {:* Return matched pattern *:}
{:allow; {:flg; partial casein replace :} p >> v :} {:* Combined *:}
{:* Upline modifier *:}
{:^declare; ... :}
{:^allow; ... :}
Wildcard rules:
. - matches any single character? - matches exactly one character* - matches zero or more characters{:* Date formatting *:}
{:date; :} {:* Unix timestamp *:}
{:date; %Y-%m-%d %H:%M:%S :} {:* Formatted date UTC *:}
{:* MD5 hash *:}
{:hash; :} {:* Random hash *:}
{:hash; text to hash :} {:* Specific hash *:}
{:* Random number *:}
{:rand; :} {:* Random integer *:}
{:rand; 1..100 :} {:* Range *:}
{:* String join *:}
{:join; |array|, | :} {:* Join with comma *:}
{:join; /array/ - /keys/ :} {:* Join keys with " - " *:}
{:* String replace *:}
{:replace; /old/new/ >> text :}
{:replace; ~ ~/~ >> path :} {:* Replace / with ~ *:}
{:* Sum *:}
{:sum; /5/10/ :} {:* Outputs 15 *:}
{:* Move content to HTML tag *:}
{:moveto; </head >> <style>css</style> :}
{:moveto; <body >> <script>js</script> :} {:* Beginning of body *:}
{:moveto; </body >> <script>js</script> :} {:* End of body *:}
{:* Count (DEPRECATED per docs) *:}
{:count; ... :} {:* Do not use *:}
{:* Upline modifiers *:}
{:^date; ... :}
{:^hash; ... :}
{:^rand; ... :}
{:^join; ... :}
{:^replace; ... :}
{:^sum; ... :}
{:^moveto; ... :}
{:* Execute Python script from JSON config *:}
{:obj; #/obj/config.json :}
{:* Inline configuration *:}
{:obj;
{
"engine": "Python",
"file": "{:;component->path:}/src/script.py",
"schema": false,
"venv": "{:;VENV_DIR:}",
"params": {
"user_id": "{:;CONTEXT->SESSION->userId:}"
},
"callback": "main",
"template": ""
}
:}
{:* With inline template *:}
{:obj;
{
"engine": "Python",
"file": "script.py",
"params": {"name": "World"}
}
>>
<div>{:;local::message:}</div>
:}
{:* Upline modifier *:}
{:^obj; ... :}
{:* Scope modifier *:}
{:+obj; ... :}
Object JSON structure:
{
"engine": "Python",
"file": "path/to/script.py",
"schema": false,
"venv": "/path/to/venv",
"params": {
"key": "value"
},
"callback": "main",
"template": "optional-template.ntpl"
}
Python script:
def main(params=None):
# Access schema if "schema": true
schema = globals().get('__NEUTRAL_SCHEMA__')
return {
"data": {
"message": f"Hello, {params.get('name', 'World')}!"
}
}
{:* Set flags for other BIFs *:}
{:flg; flag1 flag2 flag3 :}
{:* Used within other BIFs *:}
{:include; {:flg; require noparse :} >> file.txt :}
{:data; {:flg; inline :} >> {...} :}
{:snip; {:flg; static :} name >> content :}
Available flags:
require - Error if file missingnoparse - Don’t parse included filesafe - Encode all (implies noparse, encode_tags, encode_bifs)encode_tags - Encode HTML tagsencode_bifs - Encode BIF delimitersencode_tags_after - Encode after parsinginline - Inline data for data BIFstatic - Parse snippet at definition timepartial - Allow wildcard matching in allowcasein - Case-insensitive matchingreplace - Return matched pattern in allowsrc/component/cmp_XXXX_name/
├── neutral/
│ ├── component-init.ntpl {:* Global snippets (loaded at startup) *:}
│ └── route/
│ ├── index-snippets.ntpl {:* Shared across all routes *:}
│ ├── locale-en.json {:* Component translations *:}
│ ├── locale-es.json
│ ├── locale-fr.json
│ ├── locale-de.json
│ ├── data.json {:* Shared route data *:}
│ └── root/ {:* Route templates *:}
│ ├── content-snippets.ntpl {:* Root route / *:}
│ ├── data.json {:* Route data *:}
│ └── subroute/ {:* /subroute *:}
│ ├── content-snippets.ntpl
│ └── data.json
Critical Pattern: Every route MUST define current:template:body-main-content
{:* content-snippets.ntpl standard structure *:}
{:* 1. Load route data (REQUIRED) *:}
{:data; {:flg; require :} >> #/data.json :}
{:* 2. Optional: Load route-specific translations *:}
{:locale; {:flg; require :} >> #/locale.json :}
{:* 3. Include shared snippets *:}
{:!include; {:flg; require :} >> #/snippets.ntpl :}
{:* 4. Override global snippets if needed *:}
{:snip; current:template:body-carousel >> :} {:* Disable carousel *:}
{:snip; current:template:body-lateral-bar >> :} {:* Disable sidebar *:}
{:* 5. Override page heading *:}
{:snip; current:template:page-h1 >>
<div class="container my-3">
<h1 class="border-bottom p-2">{:trans; {:;local::current->route->h1:} :}</h1>
</div>
:}
{:* 6. Define main content (REQUIRED) *:}
{:snip; current:template:body-main-content >>
<div class="{:;current->theme->class->container:}">
<h3>{:trans; Page Title :}</h3>
<p>Content here...</p>
</div>
:}
{:* 7. Force output (REQUIRED - prevents 404 fallback) *:}
{:^;:}
Standard data.json:
{
"data": {
"current": {
"route": {
"title": "Page Title",
"description": "Page description for SEO",
"h1": "Visible Page Heading"
}
}
}
}
Standard index-snippets.ntpl for component:
{:* Copyright (C) 2025 https://github.com/FranBarInstance/neutral-starter-py (See LICENCE) *:}
{:* Data for all routes *:}
{:data; {:flg; require :} >> #/data.json :}
{:* Locale for current language only *:}
{:locale;
#/locale-{:lang;:}.json
:}{:else;
{:locale; #/locale-en.json :}
:}
{:* Shared snippets for all routes *:}
{:snip; component-info >>
<div>Component: {:;CURRENT_COMP_NAME:}</div>
<div>Route: {:;CURRENT_COMP_ROUTE:}</div>
:}
| Scenario | Safe | Unsafe |
|---|---|---|
| Direct output | {:;varname:} |
- |
| Array access | {:;array->key:} |
- |
| Dynamic key | {:;array->{:;key:}:} |
- |
| Full variable eval | - | {:;{:;varname:}:} ⚠️ |
| With allowlist | {:;{:allow; list >> {:;varname:} :}:} |
- |
Never do this:
{:* DANGEROUS - allows arbitrary code injection *:}
{:;{:;user_input:}:}
{:include; {:;user_file:} :}
Always use allowlists for dynamic content:
{:* SAFE - validated against allowlist *:}
{:declare; valid-pages >> home about contact :}
{:include;
{:allow; valid-pages >> {:;page:} :}
{:else; error.ntpl :}
:}
All user input is in CONTEXT and is auto-escaped:
{:* Safe - auto-escaped *:}
{:;CONTEXT->POST->message:}
{:* Dangerous - unfiltered raw output *:}
{:!;CONTEXT->POST->message:} {:* Only if you KNOW it's safe *:}
{:* NEVER - direct variable in include *:}
{:include; {:;filename:} :} {:* ERROR: insecure file name *:}
{:* NEVER - partial eval without allow *:}
{:include; page-{:;name:}.ntpl :} {:* DANGEROUS *:}
{:* ALWAYS - use allowlist *:}
{:declare; allowed >> home about :}
{:include; {:allow; allowed >> {:;name:} :}.ntpl :}
{
"config": {
"filter_all": false, {:* true = escape ALL variables *:}
"cache_disable": false, {:* true = disable all caching *:}
"disable_js": false, {:* true = manual JS injection *:}
"debug_expire": 3600, {:* Debug file expiration *:}
"debug_file": "" {:* Debug enable file path *:}
}
}
{:* Define error display snippet *:}
{:snip; form-field-error >>
{:filled; {:param; error :} >>
<div class="invalid-feedback">{:trans; {:param; error :} :}</div>
:}
:}
{:* Define form field with error handling *:}
{:snip; form-field:username >>
<div class="input-group">
<span class="input-group-text">@</span>
<input
type="text"
name="username"
value="{:;CONTEXT->POST->username:}"
class="form-control {:filled; {:param; error :} >> is-invalid :}"
>
</div>
{:param; error >> {:;form_errors->username:} :}
{:snip; form-field-error :}
:}
{:bool; HAS_SESSION >>
{:snip; user-dashboard :}
:}{:else;
{:snip; guest-welcome :}
:}
<div class="btn {:same; /{:;status:}/active/ >> btn-primary :}{:else; btn-secondary :}">
{:trans; {:;status:} :}
</div>
{:* In locale file: "Welcome back, {:;username:}" *:}
{:trans; Welcome back, {:;username:} :}
{:* When you trust the source but need HTML *:}
{:code; {:flg; noparse :} >> {:;trusted_html:} :}
{:* Prevent empty wrapper when snippet is empty *:}
{:eval; {:snip; snippet-name :} >>
<li>{:;__eval__:}</li>
:}
{:* Set parameters and include *:}
{:code;
{:param; title >> My Title :}
{:param; active >> true :}
{:include; card.ntpl :}
:}
{:* In card.ntpl *:}
<div class="card {:bool; {:param; active :} >> active :}">
<h3>{:param; title :}</h3>
</div>
| Issue | Cause | Solution |
|---|---|---|
| Template renders empty | Missing {:^;:} |
Add at end |
| 404 on valid route | Include failed | Check {:flg; require :} paths |
| Variables not showing | Wrong scope | Use {:;local::...:} for inherit data |
| Security error “insecure varname” | Direct var eval | Use {:allow; ... :} |
| Security error “insecure file name” | Direct file eval | Use {:allow; ... :} |
| Translations not loading | Locale not included | Add {:locale; ... :} |
| Cache not updating | Aggressive caching | Use {:!cache; ... :} or clear cache |
| Snippet not found | Wrong file name | Snippets must be in files with “snippet” in name |
| Parameter not available | Wrong scope | Use {:+code; ... :} to promote scope |
Templates receive data from RequestHandler classes:
# In handler.py
dispatch.schema_data["user"] = {"name": "John"} # Immutable: {:;user->name:}
dispatch.schema_local_data["items"] = ["a", "b"] # Mutable: {:;local::items:}
RequestHandler sets automatically:
CURRENT_COMP_ROUTE - Current route pathCURRENT_COMP_ROUTE_SANITIZED - Route with : instead of /CURRENT_NEUTRAL_ROUTE - Path to neutral/route directoryCURRENT_COMP_NAME - Component directory nameCURRENT_COMP_UUID - Component UUIDCONTEXT - Request context (SESSION, POST, GET, HEADERS, etc.)HAS_SESSION - “true” or nullHAS_SESSION_STR - “true” or “false”CSP_NONCE - Content Security Policy nonceLTOKEN - Link token for formsCOMPONENTS_MAP_BY_NAME - Component name → UUID mappingCOMPONENTS_MAP_BY_UUID - Component UUID → name mapping