ERB Template Engine
A powerful, Ruby ERB-inspired templating engine written in Kotlin. Generate dynamic text content (particularly LaTeX documents) from JSON data with variable interpolation, control structures, filters, custom functions, and optional type-safe schema validation.
Overview
The ErbTemplateEngine provides a flexible way to generate dynamic text content from JSON
data.
It's particularly useful for generating LaTeX documents, configuration files, and any text-based output
that requires dynamic content insertion.
Key Features
- ERB-style syntax (
<%= %>and<% %>) - Variable interpolation with dot notation for nested objects
- Built-in filters for text transformation
- Control structures (if/else, for loops)
- Custom function definitions using Groovy
- TypeScript-style schema validation
- LaTeX-specific escaping and Markdown conversion
Getting Started
Basic Usage
import com.simiacryptus.cognotik.util.ErbTemplateEngine
import com.google.gson.JsonObject
val engine = ErbTemplateEngine()
val template = "Hello, <%= name %>!"
val data = JsonObject().apply {
addProperty("name", "World")
}
val result = engine.render(template, data)
// Output: "Hello, World!"
Input Data Format
The engine accepts data as a JsonObject from the Gson library. Data can include:
- Primitives: strings, numbers, booleans
- Objects: nested JSON objects
- Arrays: JSON arrays for iteration
val data = JsonObject().apply {
addProperty("title", "My Document")
addProperty("count", 42)
addProperty("active", true)
add("user", JsonObject().apply {
addProperty("name", "John")
addProperty("email", "john@example.com")
})
add("items", JsonArray().apply {
add("Item 1")
add("Item 2")
add("Item 3")
})
}
Basic Syntax
The engine uses ERB-style delimiters:
| Syntax | Purpose | Example |
|---|---|---|
<%= expr %> |
Output expression | <%= name %> |
<% code %> |
Control structure | <% if active %> |
<%# comment %> |
Comment (in preamble) | <%# @type ... %> |
Whitespace Handling
Whitespace inside tags is flexible:
<%=name%> <%-- Works --%>
<%= name %> <%-- Works --%>
<%= name %> <%-- Works --%>
Variable Interpolation
Simple Variables
Access top-level variables directly:
<%= title %>
<%= count %>
<%= active %>
Nested Object Access
Use dot notation to access nested properties:
<%= user.name %>
<%= user.email %>
<%= config.settings.theme %>
Missing Variables
Missing variables render as empty strings (no error):
<%= nonexistent %> <%-- Outputs: "" --%>
Filters
Filters transform values using the pipe (|) syntax. Multiple filters can be chained.
Syntax
<%= value | filter %>
<%= value | filter:argument %>
<%= value | filter1 | filter2 | filter3 %>
Built-in Filters
escape
Escapes special LaTeX characters:
| Input | Output |
|---|---|
\ |
\textbackslash{} |
{ |
\{ |
} |
\} |
$ |
\$ |
& |
\& |
% |
\% |
# |
\# |
_ |
\_ |
~ |
\textasciitilde{} |
^ |
\textasciicircum{} |
markdown
Converts basic Markdown to LaTeX:
| Markdown | LaTeX |
|---|---|
**bold** |
\textbf{bold} |
*italic* |
\textit{italic} |
`code` |
\texttt{code} |
[text](url) |
\href{url}{text} |
upper / lower
Converts text to uppercase or lowercase:
<%= name | upper %> <%-- "hello" → "HELLO" --%>
<%= name | lower %> <%-- "HELLO" → "hello" --%>
join
Joins array elements with a separator:
<%= items | join %> <%-- Default: ", " --%>
<%= items | join:' - ' %> <%-- Custom separator --%>
<%= items | join:'\n' %> <%-- Newline separator --%>
default
Provides a fallback value for empty or missing values:
<%= name | default:'Unknown' %>
<%= color | default:"20, 20, 35" %>
Chaining Filters
Filters are applied left to right:
<%= name | upper | escape %>
<%-- "hello & world" → "HELLO \& WORLD" --%>
Custom Filters
Register custom filters programmatically:
engine.registerFilter("reverse") { value, args ->
value?.toString()?.reversed() ?: ""
}
engine.registerFilter("truncate") { value, args ->
val maxLength = args.firstOrNull()?.toIntOrNull() ?: 50
val text = value?.toString() ?: ""
if (text.length > maxLength) {
text.take(maxLength) + "..."
} else {
text
}
}
Control Structures
If Statements
Basic If
<% if condition %>
Content when true
<% end %>
If-Else
<% if condition %>
Content when true
<% else %>
Content when false
<% end %>
Truthiness Rules
| Value Type | Truthy | Falsy |
|---|---|---|
| Boolean | true |
false |
| String | Non-empty | Empty "" |
| Number | Always truthy | - |
| Array | Non-empty | Empty [] |
| Object | Non-empty | Empty {} |
| Null | - | Always falsy |
Comparison Operators
<% if status == "active" %>Active<% end %>
<% if status != "inactive" %>Not Inactive<% end %>
<% if !hidden %>Visible<% end %>
Arithmetic in Conditions
Simple arithmetic expressions (like modulo) can be used in conditions:
<% if loop.index % 2 == 0 %>Even row<% end %>
For Loops
Iterating Over Arrays
<% for item in items %>
<%= item %>
<% end %>
Loop Variables
Inside a loop, a loop object provides metadata:
| Variable | Description |
|---|---|
loop.index |
Current index (0-based) |
loop.first |
true if first iteration |
loop.last |
true if last iteration |
<% for item in items %>
<%= loop.index %>: <%= item %><% if !loop.last %>, <% end %>
<% end %>
Iterating Over Objects
When iterating over an object, each entry has key and value properties:
<% for entry in settings %>
<%= entry.key %> = <%= entry.value %>
<% end %>
Nested Loops
<% for row in matrix %>
<% for cell in row %>
<%= cell %>
<% end %>
<% end %>
Combining Loops and Conditionals
If-Else Inside For Loops
<% for item in items %>
<% if item.active %>Active: <%= item.name %><% else %>Inactive: <%= item.name %><% end %>
<% end %>
For Loops Inside Conditionals
<% if showItems %>
<% for item in items %><%= item %> <% end %>
<% end %>
Custom Functions
Define reusable functions using Groovy syntax within templates.
Defining Functions
<% def functionName(param1, param2) %>
// Groovy code here
return result
<% enddef %>
Basic Function Example
<% def greet(name) %>
return "Hello, " + name + "!"
<% enddef %>
<%= greet("World") %>
<%-- Output: Hello, World! --%>
Functions with Multiple Parameters
<% def formatName(first, last) %>
return last + ", " + first
<% enddef %>
<%= formatName("John", "Doe") %>
<%-- Output: Doe, John --%>
Using Functions as Filters
Defined functions are automatically available as filters:
<% def double(x) %>
return x * 2
<% enddef %>
<%= value | double %>
Accessing Global Data from Functions
Functions can access the template's global data through the data variable:
<% def greetWithTitle(name) %>
return data.title + " " + name
<% enddef %>
<%= greetWithTitle("World") %>
<%-- With data.title = "Hello", Output: Hello World --%>
Using Functions in For Loops
Functions work seamlessly within iteration contexts:
<% def format(item) %>
return "[" + item + "]"
<% enddef %>
<% for item in items %><%= item | format %><% if !loop.last %> <% end %><% end %>
<%-- Output: [a] [b] [c] --%>
Chaining Functions with Filters
Custom functions can be chained with built-in filters:
<% def prefix(text) %>
return "PREFIX_" + text
<% enddef %>
<%= name | prefix | upper %>
<%-- "test" becomes "PREFIX_TEST" --%>
Type Schema and Validation
The engine supports TypeScript-style type declarations for documenting and validating template data.
Schema Preamble Syntax
Place the schema at the beginning of the template:
---
<%#
@type TemplateData = {
fieldName: type;
optionalField?: type;
};
%>
Template content here...
Supported Types
| Type | Description | Example |
|---|---|---|
string |
Text values | name: string; |
number |
Numeric values | age: number; |
boolean |
True/false | active: boolean; |
any |
Any value | data: any; |
type[] |
Array of type | items: string[]; |
Array<type> |
Array (generic) | items: Array<number>; |
{ ... } |
Nested object | user: { name: string }; |
type1 | type2 |
Union type | id: string | number; |
Enabling Strict Validation
val engine = ErbTemplateEngine()
engine.strictValidation = true
try {
engine.render(template, data)
} catch (e: ErbTemplateEngine.TemplateValidationException) {
println("Validation errors:")
e.errors.forEach { error ->
println(" ${error.path}: ${error.message}")
}
}
LaTeX-Specific Features
Escaping for LaTeX
Always use the escape filter for user-provided content:
\section{<%= title | escape %>}
<%= content | escape %>
Markdown in LaTeX
Convert Markdown formatting to LaTeX:
<%= description | markdown %>
Document Structure Pattern
\documentclass{article}
\usepackage{hyperref}
\title{<%= title | escape %>}
\author{<%= author | escape %>}
\date{<%= date | default:'\today' %>}
\begin{document}
\maketitle
<% if showToc %>
\tableofcontents
<% end %>
<% for section in sections %>
\section{<%= section.title | escape %>}
<%= section.content | markdown %>
<% end %>
\end{document}
The engine correctly distinguishes between LaTeX's \end{...} commands and the template's
<% end %> tags.
Tables Pattern
\begin{tabular}{|l|r|}
\hline
\textbf{Name} & \textbf{Value} \\
\hline
<% for row in data %>
<%= row.name | escape %> & <%= row.value | escape %> \\
<% end %>
\hline
\end{tabular}
Lists Pattern
\begin{itemize}
<% for item in items %>
\item <%= item | escape %>
<% end %>
\end{itemize}
API Reference
ErbTemplateEngine Class
Constructor
val engine = ErbTemplateEngine()
Properties
| Property | Type | Default | Description |
|---|---|---|---|
strictValidation |
Boolean |
false |
Throw exception on validation errors |
Methods
| Method | Description |
|---|---|
render(template: String, data: JsonObject): String |
Renders a template with the provided data |
registerFilter(name: String, filter: (Any?, List<String>) -> String) |
Registers a custom filter |
extractSchema(template: String): TypeSchema? |
Extracts the type schema from a template's preamble |
callFunction(functionName: String, firstArg: Any?, additionalArgs: List<String>):
String |
Calls a defined function programmatically |
TypeSchema Class
Methods
| Method | Description |
|---|---|
toTypeScript(): String |
Generates a TypeScript interface definition |
FieldType Sealed Class
Represents field types in the schema:
StringTypeNumberTypeBooleanTypeAnyTypeArrayTypeObjectTypeUnionTypeCustomType
ValidationError Data Class
data class ValidationError(
val path: String, // e.g., "user.profile.name"
val message: String // e.g., "Required field is missing"
)
TemplateValidationException
Thrown when strict validation fails:
class TemplateValidationException(val errors: List<ValidationError>)
Best Practices
1. Always Escape User Content
<%-- Good --%>
<%= userInput | escape %>
<%-- Bad - potential LaTeX injection --%>
<%= userInput %>
2. Use Default Values
<%-- Good --%>
<%= subtitle | default:'No subtitle' %>
<%-- Risky - may produce empty output --%>
<%= subtitle %>
3. Define Schemas for Complex Templates
---
<%#
@type TemplateData = {
title: string;
sections: Array<{ title: string; content: string }>;
};
%>
---
4. Use Functions for Repeated Logic
<% def formatDate(date) %>
return date.split("-").reverse().join("/")
<% enddef %>
Published: <%= publishDate | formatDate %>
Updated: <%= updateDate | formatDate %>
5. Handle Empty Collections
<% if items %>
\begin{itemize}
<% for item in items %>
\item <%= item | escape %>
<% end %>
\end{itemize}
<% else %>
No items available.
<% end %>
6. Use Meaningful Variable Names
<%-- Good --%>
<% for chapter in chapters %>
<% for section in chapter.sections %>
<%-- Less clear --%>
<% for c in chapters %>
<% for s in c.sections %>
Examples
Complete LaTeX Document
---
<%#
@type TemplateData = {
title: string;
author: string;
date?: string;
abstract?: string;
chapters: Array<{
title: string;
sections: Array<{
title: string;
content: string;
}>;
}>;
bibliography?: string[];
};
%>
\documentclass[12pt]{report}
\usepackage[utf8]{inputenc}
\usepackage{hyperref}
\title{<%= title | escape %>}
\author{<%= author | escape %>}
\date{<%= date | default:'\today' %>}
\begin{document}
\maketitle
<% if abstract %>
\begin{abstract}
<%= abstract | markdown %>
\end{abstract}
<% end %>
\tableofcontents
<% for chapter in chapters %>
\chapter{<%= chapter.title | escape %>}
<% for section in chapter.sections %>
\section{<%= section.title | escape %>}
<%= section.content | markdown %>
<% end %>
<% end %>
<% if bibliography %>
\begin{thebibliography}{99}
<% for ref in bibliography %>
\bibitem{ref<%= loop.index %>} <%= ref | escape %>
<% end %>
\end{thebibliography}
<% end %>
\end{document}
Invoice Template
<% def formatCurrency(amount) %>
return String.format("$%.2f", amount)
<% enddef %>
<% def calculateTotal(items) %>
return items.collect { it.quantity * it.price }.sum()
<% enddef %>
\documentclass{article}
\usepackage{booktabs}
\begin{document}
\textbf{INVOICE}
\vspace{1em}
\textbf{To:} <%= customer.name | escape %> \\
<%= customer.address | escape %>
\vspace{1em}
\begin{tabular}{lrrr}
\toprule
\textbf{Item} & \textbf{Qty} & \textbf{Price} & \textbf{Total} \\
\midrule
<% for item in items %>
<%= item.name | escape %> & <%= item.quantity %> & <%= item.price | formatCurrency %> & <%= (item.quantity * item.price) | formatCurrency %> \\
<% end %>
\midrule
\textbf{Total} & & & \textbf{<%= items | calculateTotal | formatCurrency %>} \\
\bottomrule
\end{tabular}
\end{document}
Error Handling
Common Errors
| Error | Cause | Solution |
|---|---|---|
| Empty output | Missing variable | Use default filter |
| Validation exception | Missing required field | Provide all required data |
| Function not defined | Calling undefined function | Define function before use |
| Malformed template | Unclosed tags | Check <% %> matching |
Debugging Tips
- Check variable paths: Use simple interpolation to verify data access
- Test incrementally: Build templates piece by piece
- Enable strict validation: Catch data issues early
- Log intermediate values: Use custom filters for debugging
engine.registerFilter("debug") { value, _ ->
println("DEBUG: $value")
value?.toString() ?: ""
}
<%= data | debug %>