Architecture
NLT uses a clean two-phase pipeline: template text is first parsed into an AST, then the AST is walked to produce the final HTML output. The entire engine is implemented with zero third-party dependencies.
Two-Phase Pipeline
The two phases are fully decoupled. The tokenizer produces a list[ASTNode] that the renderer consumes. You can inspect the AST directly by calling tokenize() without rendering.
Module Structure
| Module | Responsibility |
|---|---|
__init__.py | Public API surface. Re-exports all public types and functions. |
renderer.py | AST to HTML rendering. Data resolution. Math operations. Expression dispatch. Template inheritance. Macro invocation. |
tokenizer.py | Template string to AST node list. Block classification. Nested structure building. Comment stripping. HTML artifact cleaning. |
parser.py | Expression text to NLTExpression dataclass. Source identification. Math extraction. Modifier and filter parsing. |
nodes.py | All 14 AST node dataclasses plus supporting types (ConditionClause, CompoundCondition, ListConfig, TableConfig). |
condition_parser.py | {if} condition text to AnyCondition tree. Operator matching. AND/OR splitting. NOT negation via De Morgan's law. |
comparators.py | Condition evaluation. Numeric, string, date, containment, and existence comparisons. |
formatters.py | Currency formatting (17 currencies). Date formatting (presets and custom tokens). Percentage formatting. Pluralization. Title case. |
filters.py | 19 filter implementations. Filter dispatch map. Pure functions: (value, arg) -> value. |
source_parser.py | Shared source phrase extraction used by tokenizer, list_parser, and table_parser. Extracts (source, attribute_path, event_name) tuples. |
list_parser.py | {list ...} block text to ListConfig. Style and limit parsing. |
table_parser.py | {table ...} block text to TableConfig. Column extraction. |
converter.py | Bidirectional NLT/Jinja2 conversion. Template language detection. Expression counting. |
Expression Resolution Order
When the renderer encounters an ExpressionNode, it tries these resolution strategies in order. The first non-None result wins:
- Loop variables --
{item.Name},{loop.index}(checked from innermost to outermost loop scope) - Macro parameters --
{name}inside a{define}body (checked from innermost to outermost macro scope) - Local variables --
{discount}set via{set discount to ...} - Ternary expressions --
{condition ? "true_val" : "false_val"} - Built-in variables --
{today},{now} - Standard NLT expression --
{the user's ...},{the ... from the trigger event}, etc. Parsed viaparse_nlt_expression().
If none match and parsing fails, the original {...} text is emitted unchanged. This is intentional -- graceful degradation means a typo never crashes the rendering pipeline.
The resolution order matters when names collide. A loop variable {item} will shadow a local variable with the same name. A macro parameter {name} will shadow a local variable {name} set outside the macro.
Security Model
XSS Prevention
All user-supplied data resolved from the contact, events, or computed values is HTML-escaped before insertion into the output:
| Character | Replacement |
|---|---|
& | & |
< | < |
> | > |
" | " |
This applies to expression values, list items, table cells, aggregate results, loop item properties, ternary results, built-in variable output, and macro output.
Template-authored HTML (the literal text written by the template creator) is not escaped. Only data resolved from the runtime context is escaped.
No Code Execution
NLT is a pure data resolver. It cannot:
- Import or call Python modules
- Execute arbitrary code (no
eval, noexec) - Access the filesystem
- Make network requests
- Modify the input data context
- Access environment variables or process state
Sandboxed Context
The renderer only accesses data explicitly provided via the constructor:
contactdicttrigger_eventdictadditional_eventsdictevent_store(Protocol -- caller-controlled)template_loader(Protocol -- caller-controlled)
There is no implicit access to global state, request context, database connections, or system resources.
Content inside {raw}...{end raw} blocks is emitted verbatim without HTML escaping. This is by design -- raw blocks are authored by template creators (trusted), not populated from user data.
Zero Dependencies
The entire engine uses only the Python standard library: re, dataclasses, datetime, urllib.parse, and typing. This eliminates supply chain risk and ensures the package installs instantly with no transitive dependencies.
Next Steps
- Extension Points -- implement custom EventStore and TemplateLoader protocols
- API Reference: Renderer -- complete method signatures and parameter details
- API Reference: AST Nodes -- all 14 node types with field descriptions