# Template Library (`lib/tpl`) A dual-engine template rendering library. The **simple engine** handles lightweight `$variable` substitution with pipe modifiers. The **full engine** supports PHP-style ` ?>` code blocks with expressions, control flow, loops, and the complete built-in function library. ```go import "github.com/getevo/evo/v2/lib/tpl" ``` --- ## Table of Contents 1. [Simple Engine](#simple-engine) - [Variable Substitution](#variable-substitution) - [Pipe Modifiers](#pipe-modifiers) - [Filter Chaining](#filter-chaining) - [Function Calls](#function-calls) - [Missing Variable Fallback](#missing-variable-fallback) - [Dollar Escape](#dollar-escape) - [Multiple Data Sources](#multiple-data-sources) 2. [Full Engine](#full-engine) - [Code Blocks](#code-blocks) - [Variable Interpolation in Text](#variable-interpolation-in-text) - [Statements](#statements) - [Expressions](#expressions) - [Array and Map Literals](#array-and-map-literals) - [Control Flow](#control-flow) - [Loops](#loops) - [Loop Metadata](#loop-metadata) - [Include / Require](#include--require) 3. [Built-in Functions](#built-in-functions) 4. [Custom Functions](#custom-functions) 5. [API Reference](#api-reference) --- ## Simple Engine The simple engine processes plain text containing `$variable` placeholders. No code blocks are needed — variables are replaced wherever they appear. ### Variable Substitution Supported path forms: | Syntax | Description | |---------------------|--------------------------------------| | `$name` | Plain variable | | `$obj.Field` | Struct field or map key (dot access) | | `$arr[0]` | Integer index | | `$arr["key"]` | String map key | | `$arr[0].Field` | Chained access | | `$obj.Sub.Field` | Deeply nested field | ```go // Structs type User struct { Name string Email string } u := User{Name: "Alice", Email: "alice@example.com"} tpl.Render("Hello $Name, your email is $Email", u) // → "Hello Alice, your email is alice@example.com" // Maps data := map[string]any{"city": "Paris", "country": "France"} tpl.Render("$city, $country", data) // → "Paris, France" // Nested struct fields type Address struct{ Street string } type Person struct{ Name string; Addr Address } p := Person{Name: "Bob", Addr: Address{Street: "Main St"}} tpl.Render("$Name lives at $Addr.Street", p) // → "Bob lives at Main St" // Array indexing items := map[string]any{"tags": []string{"go", "tpl", "fast"}} tpl.Render("First tag: $tags[0], second: $tags[1]", items) // → "First tag: go, second: tpl" // Chained access: array of maps data := map[string]any{ "users": []map[string]any{ {"name": "Alice"}, {"name": "Bob"}, }, } tpl.Render("$users[0][name] and $users[1][name]", data) // → "Alice and Bob" ``` ### Pipe Modifiers Append `|modifier` to a variable to transform its value on output. **Built-in transforms:** | Modifier | Description | |----------|------------------------------------------| | `upper` | Convert to UPPERCASE | | `lower` | Convert to lowercase | | `title` | Title Case | | `trim` | Strip leading and trailing whitespace | | `html` | HTML-escape (`<`, `>`, `&`, `"`, `'`) | | `url` | URL query-encode | | `json` | JSON-encode the value | ```go tpl.Render("$name|upper", tpl.Pairs("name", "alice")) // → "ALICE" tpl.Render("$title|title", tpl.Pairs("title", "hello world")) // → "Hello World" tpl.Render("$comment|html", tpl.Pairs("comment", "")) // → "<script>alert(1)</script>" tpl.Render("$query|url", tpl.Pairs("query", "hello world")) // → "hello+world" tpl.Render("$data|json", tpl.Pairs("data", map[string]any{"k": 1})) // → `{"k":1}` // Registered functions also work as modifiers (see Custom Functions) tpl.RegisterFunc("exclaim", func(s string) string { return s + "!" }) tpl.Render("$greeting|exclaim", tpl.Pairs("greeting", "Hello")) // → "Hello!" ``` ### Filter Chaining Chain multiple modifiers with `|`. They are applied left to right. ```go tpl.Render("$name|trim|upper", tpl.Pairs("name", " alice ")) // → "ALICE" tpl.Render("$name|trim|lower|title", tpl.Pairs("name", " JOHN DOE ")) // → "John Doe" tpl.Render("$items|count", tpl.Pairs("items", []int{1, 2, 3, 4, 5})) // → "5" (using the registered count function) // Chain a builtin and a custom function tpl.RegisterFunc("slug", func(s string) string { return strings.ReplaceAll(strings.ToLower(s), " ", "-") }) tpl.Render("$title|trim|slug", tpl.Pairs("title", " My Blog Post ")) // → "my-blog-post" ``` ### Function Calls Call a registered function with arguments using `$fn(args...)`. Arguments can be `$variables`, string literals, or numeric literals. ```go tpl.RegisterFunc("greet", func(name, lang string) string { if lang == "es" { return "Hola, " + name + "!" } return "Hello, " + name + "!" }) tpl.Render(`$greet($name, "es")`, tpl.Pairs("name", "Maria")) // → "Hola, Maria!" tpl.Render(`$greet("World", "en")`) // → "Hello, World!" // String escapes in double-quoted args: \n \t \" \\ tpl.Render(`$sprintf("%-10s %d", $name, $score)`, tpl.Pairs("name", "Alice", "score", 42)) // → "Alice 42" ``` ### Missing Variable Fallback When a variable is not found, the modifier string is used as a literal default value (if it doesn't match any known modifier name or registered function). ```go tpl.Render("$nickname|Anonymous", map[string]any{}) // → "Anonymous" (nickname not set → use literal "Anonymous") tpl.Render("Dear $title|Sir,", map[string]any{}) // → "Dear Sir," ``` Built-in transforms on missing variables keep the placeholder unchanged: ```go tpl.Render("$name|upper", map[string]any{}) // → "$name|upper" (kept as-is because upper is a known transform) ``` ### Dollar Escape Use `$$` to output a literal `$`. ```go tpl.Render("Price: $$price", tpl.Pairs("price", 9.99)) // → "Price: $price" (not interpolated — $$ became $, then "price" is literal) tpl.Render("Cost: $$$amount", tpl.Pairs("amount", "5")) // → "Cost: $5" ($$ → $, then $amount → "5") ``` ### Multiple Data Sources Pass multiple structs, maps, or slices. Variables are looked up in each source in order; the first match wins. ```go type App struct{ Name, Version string } type User struct{ Name, Role string } app := App{Name: "MyApp", Version: "2.0"} user := User{Name: "Alice", Role: "admin"} tpl.Render("$app.Name $app.Version — logged in as $user.Name ($user.Role)", app, user) // Works but requires "app" and "user" keys // OR use tpl.Pairs to mix: tpl.Render("Welcome $name to $app", tpl.Pairs("name", "Alice", "app", "MyApp")) // → "Welcome Alice to MyApp" ``` ### `tpl.Pairs` Helper to build a `map[string]any` from flat alternating key/value pairs. ```go data := tpl.Pairs("name", "Alice", "age", 30, "active", true) // → map[string]any{"name": "Alice", "age": 30, "active": true} tpl.Render("$name is $age years old", data) // → "Alice is 30 years old" ``` --- ## Full Engine The full engine parses templates with ` ?>` code blocks. Text outside code blocks is output verbatim (with `$var` interpolation). Code blocks contain statements, control flow, loops, and expressions. ### Code Blocks Everything inside ` ?>` is executed as code. Text outside is printed as-is with `$var` substitution. ``` Hello echo $name ?>! Today is echo date("2006-01-02", now()) ?>. ``` Use the `Builder` API (recommended) or the lower-level `CompileEngine`: ```go // Builder from inline string result := tpl.Text(`Hello echo $name ?>!`).Set(tpl.Pairs("name", "Alice")).Render() // → "Hello Alice!" // Builder from file (auto-cached, mtime-invalidated) result := tpl.File("views/page.html").Set(user).Set(app).Render() // Write to an io.Writer tpl.File("layout.html").Set(data).RenderWriter(w) // Lower-level engine := tpl.CompileEngine(src) ctx := tpl.Pairs("x", 42) result := tpl.RenderText(src, ctx) // Convenience helpers tpl.RenderText(` echo $x * 2 ?>`, tpl.Pairs("x", 21)) // → "42" tpl.RenderFile("page.html", tpl.Pairs("title", "Home")) ``` ### Variable Interpolation in Text `$variable` references in plain text (outside ` ?>`) resolve against the current context, with the same path syntax as the simple engine. ``` $user = {name: "Alice", score: 99} ?> Player: $user.name Score: $user.score Greeting: $user["name"] ``` Missing variables are kept as-is: `$unknown` → `$unknown`. ### Statements #### Output: `echo` / `print` ``` echo "Hello, " . $name . "!" ?> print $total * 1.2 ?> $greeting ?> upper($name) ?> ``` #### Assignment ``` $x = 42 ?> $msg := "Hello" ?> $sum = $a + $b ?> ``` #### Compound Assignment ``` $count += 1 ?> $total -= $discount ?> $price *= 1.1 ?> $half /= 2 ?> ``` #### Increment / Decrement ``` $i++ ?> $j-- ?> ``` ### Expressions #### Literals ``` echo 42 ?> echo 3.14 ?> echo "hello" ?> echo 'world' ?> echo true ?> echo false ?> echo null ?> ``` #### Arithmetic ``` echo 2 + 3 ?> echo 10 - 4 ?> echo 3 * 7 ?> echo 15 / 4 ?> echo 17 % 5 ?> echo -$n ?> ``` #### String Concatenation Use `.` (dot operator) to concatenate strings: ``` echo "Hello, " . $name . "!" ?> $fullName = $first . " " . $last ?> ``` #### Comparison ``` echo $a == $b ?> echo $a != $b ?> echo $a < $b ?> echo $a > $b ?> echo $a <= $b ?> echo $a >= $b ?> ``` #### Logical ``` echo $x && $y ?> echo $x || $y ?> echo !$flag ?> ``` #### Ternary ``` echo $score >= 60 ? "pass" : "fail" ?> $label = $active ? "enabled" : "disabled" ?> echo isset($name) ? $name : "Guest" ?> ``` #### Null Coalescing Returns the left side if it is non-nil, otherwise the right side: ``` echo $name ?? "Anonymous" ?> $display = $nickname ?? $username ?? "User" ?> ``` #### `isset` — Variable Check ``` if(isset($user)){ ?> Hello, echo $user ?>! } ?> ``` #### Variable Access ``` echo $name ?> echo $user.Name ?> echo $arr[0] ?> echo $map["key"] ?> echo $arr[$i] ?> echo $users[0]["name"] ?> echo $data.Items[2].Title ?> ``` ### Array and Map Literals Construct arrays and maps inline inside code blocks. #### Array Literals ``` $nums = [1, 2, 3, 4, 5] ?> echo $nums[2] ?> $mixed = ["hello", 42, true, null] ?> echo len($mixed) ?> $computed = [$a + 1, $b * 2, $c] ?> echo json([10, 20, 30]) ?> echo len([1, 2, 3]) ?> for($v := range ["a", "b", "c"]){ ?>$v } ?> $matrix = [[1, 2], [3, 4]] ?> echo $matrix[1][0] ?> $list = $active ? [1, 2, 3] : [] ?> ``` #### Map Literals Keys can be quoted strings or bare identifiers: ``` $user = {"name": "Alice", "age": 30} ?> echo $user["name"] ?> echo $user["age"] ?> $cfg = {host: "localhost", port: 3306} ?> echo $cfg["host"] ?> $m = {"double": $x * 2, "label": "val=" . $x} ?> echo json({"status": "ok", "code": 200}) ?> for($k, $v := range {"a": 1, "b": 2}){ ?>$k=$v } ?> $config = {"db": {"host": "localhost", "port": 5432}} ?> echo $config["db"]["host"] ?> $users = [{"name": "Alice"}, {"name": "Bob"}] ?> echo $users[0]["name"] ?> echo $users[1]["name"] ?> ``` ### Control Flow #### If / Else ``` if($score >= 90){ ?> Grade: A }else if($score >= 80){ ?> Grade: B }else if($score >= 70){ ?> Grade: C }else{ ?> Grade: F } ?> ``` ``` if($user.IsAdmin && $user.Active){ ?> Admin Panel } ?> ``` ``` if(!isset($token) || $token == ""){ ?> Please log in. } ?> ``` #### Switch `break` exits the switch. Multiple values per case are comma-separated. ``` switch($status){ ?> case "active": ?> User is active. case "pending", "invited": ?> Awaiting confirmation. case "banned": ?> Access denied. default: ?> Unknown status. } ?> ``` ``` switch($role){ ?> case "admin": ?>admin break ?> case "editor": ?>editor break ?> default: ?>viewer } ?> ``` ### Loops #### For-Range (iterate over collection) Iterates over slices, arrays, maps, strings (rune by rune), or integers (0 to n-1). **Value only:** ``` for($v := range $items){ ?>