{"id":39742433,"url":"https://github.com/karlseguin/ztl","last_synced_at":"2026-01-18T11:16:28.687Z","repository":{"id":269710424,"uuid":"896434747","full_name":"karlseguin/ztl","owner":"karlseguin","description":"Templating Language for Zig","archived":false,"fork":false,"pushed_at":"2025-06-28T01:07:37.000Z","size":571,"stargazers_count":32,"open_issues_count":1,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-06-28T02:20:37.448Z","etag":null,"topics":["template-engine","zig-library","zig-package"],"latest_commit_sha":null,"homepage":"","language":"Zig","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/karlseguin.png","metadata":{"files":{"readme":"readme.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2024-11-30T11:01:48.000Z","updated_at":"2025-06-28T01:07:40.000Z","dependencies_parsed_at":"2025-06-28T02:19:48.600Z","dependency_job_id":"38016cda-37ff-41a7-88e5-4ff92d766811","html_url":"https://github.com/karlseguin/ztl","commit_stats":null,"previous_names":["karlseguin/ztl"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/karlseguin/ztl","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karlseguin%2Fztl","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karlseguin%2Fztl/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karlseguin%2Fztl/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karlseguin%2Fztl/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/karlseguin","download_url":"https://codeload.github.com/karlseguin/ztl/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/karlseguin%2Fztl/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28535161,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-18T10:13:46.436Z","status":"ssl_error","status_checked_at":"2026-01-18T10:13:11.045Z","response_time":98,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["template-engine","zig-library","zig-package"],"created_at":"2026-01-18T11:16:28.618Z","updated_at":"2026-01-18T11:16:28.669Z","avatar_url":"https://github.com/karlseguin.png","language":"Zig","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Zig Template Language\n\n```zig\nconst std = @import(\"std\");\nconst ztl = @import(\"ztl\");\n\nconst Product = struct {\n    name: []const u8,\n};\n\npub fn main() !void {\n    var gpa = std.heap.GeneralPurposeAllocator(.{}){};\n    const allocator = gpa.allocator();\n\n    var template = ztl.Template(void).init(allocator, {});\n    defer template.deinit();\n\n    var compile_error_report = ztl.CompileErrorReport{};\n\n    // The templating language is erb-inspired\n    template.compile(\n        \\\\ \u003ch2\u003eProducts\u003c/h2\u003e\n        \\\\ \u003c% foreach (@products) |product| { -%\u003e\n        \\\\     \u003c%= escape product[\"name\"] %\u003e\n        \\\\ \u003c% } %\u003e\n    , .{.error_report = \u0026compile_error_report}) catch |err| {\n        std.debug.print(\"{}\\n\", .{compile_error_report});\n        return err;\n    };\n\n    // Write to any writer, here we're using an ArrayList\n    var buf = std.ArrayList(u8).init(allocator);\n    defer buf.deinit();\n\n    var render_error_report = ztl.RenderErrorReport{};\n\n    // The render method is thread-safe.\n    template.render(buf.writer(), .{\n        .products = [_]Product{\n            .{.name = \"Keemun\"},\n            .{.name = \"Silver Needle\"},\n        }\n    }, .{.error_report = \u0026render_error_report}) catch |err| {\n        defer render_error_report.deinit();\n        std.debug.print(\"{}\\n\", .{render_error_report});\n        return err;\n    };\n\n    std.debug.print(\"{s}\\n\", .{buf.items});\n}\n```\n\n## Project Status\nThe project is in early development and has not seen much dogfooding. Looking for feature requests and bug reports.\n\n## Template Overview\nOutput tags, `\u003c%= %\u003e`, support space trimming via `\u003c%-=` and `-%\u003e`. \n\nBy default, output is not escaped. You can use the `escape` keyword to apply basic HTML encoding:\n\n```\n\u003c%= escape product[\"name\"] %\u003e\n```\n\nAlternatively, you can set `ZtlConfig.escape_by_default` (see [customization](#customization)) to true to have escape on by default. In this case the special `escape` is invalid, however the `safe` keyword can be used to output the value as-is.\n\n### Types\nThe language supports the following types:\n* i64\n* f64\n* bool\n* null\n* string\n* list\n* map\n\n```js\nvar i = 0;\nvar active = true;\nvar tags = [1, 1.2, null];\n\n// Map keys must be strings or integers\n// In a map initialization, the quotes around simple string keys are optional\nvar lookup = %{tea: 9, 123: null};\n```\n\nStrings are wrapped in single-quotes, double-quotes or backticks (\\`hello\\`). Backticks do not require escaping.\n\nThere are only a few properties:\n* `len` - get the length of a list or map\n* `key` - get the key of a map entry (only valid in a `foreach`)\n* `value` - get the value of a map entry (only valid in a `foreach`)\n\nThere are currently only a few methods:\n* `pop` - return and remove the last value from a list (or `null`)\n* `last` - return the last value from a list (or `null`)\n* `first` - return the first value from a list (or `null`)\n* `append` - add a value to a list\n* `remove` - remove the given value from a list (O(n)) or a map\n* `remove_at` - remove the value from a list at the given index (supports negative indexes)\n* `contains` - returns true/false if the value exists in a list (O(n)) or map\n* `index_of` - returns the index of (or null) of the first instance of the value in a list (O(n))\n* `sort` - sorts the list in-place\n* `concat` - appends one array to another, mutating the original\n\n```js\nvar list = [3, 2, 1].sort();\nfor (list) |item| {\n    \u003c%= item %\u003e\n}\n```\n\n### Control Flow\nSupported control flow:\n* if / else if / else \n* while\n* for (;;)\n* foreach\n* orelse\n* ternary (`?;`)\n* break / continue\n\nForeach works over lists and maps only. Multiple values can be given. Iterations stops once any of the values is done:\n\n```js\n// this will only do 2 iterations\n// since the map only has 2 entries\nforeach([1, 2, 3], %{a: 1, b: 2}) |a, b| {\n    \u003c%= a + b.value %\u003e \n}\n```\n\n(Internally, a `foreach` makes use of 3 extra types: list_iterator, map_iterator and map_entry, but there's no way to create these explicitly).\n\n`break` and `continue` take an optional integer operand to control how many level to break/continue:\n\n```js\nfor (var i = 0; i \u003c arr1.len; i++) {\n    for (var j = 0; j \u003c arr2.len; j++) {\n        if (arr2[j] \u003e arr1[i]) break 2;\n    }\n}\n```\n\n### Functions\nTemplates can contain functions:\n\n```js\nadd(1, 2);\n\n// can be declared before or after usage\nfn add(a, b) {\n    return a + b;\n}\n```\n\n\n## Zig Usage\nYou can get an error report on failed compile by passing in a `*ztl.CompileErrorReport`:\n\n```zig\nvar template = ztl.Template(void).init(allocator, {});\ndefer template.deinit();\n\nvar error_report = ztl.CompileErrorReport{};\ntemplate.compile(\"\u003c% 1.invalid() %\u003e\", .{.error_report = \u0026error_report}) catch |err| {\n    std.debug.print(\"{}\\n\", .{error_report});\n    return err;\n};\n```\n\nThe `template.render` method is thread-safe. The general intention is that a template is compiled once and rendered multiple times. The `render` method takes an optional `RenderOption` argument.\n\nThe first optional field is `*ztl.RenderErorrReport`. When set, a description of the runtime error can be retreived. When set, you must call `deinit` on the error report on error:\n\n```zig\nvar error_report = ztl.RenderErrorReport{};\ntemplate.render(buf.writer(), .{}, .{.error_report = \u0026error_report}) catch |err| {\n    defer error_report.deinit();\n    std.debug.print(\"Runtime error: {}\\n\", .{error_report});\n};\n```\n\nThe second optional field is `allocator`. When omitted, the allocator given to `Template.init` is used. This allows the template as whole to have a long-lived allocator, but the `render` method to have a specific short-term allocator. For example, if you're rendering an HTML response, the render allocator might be tied to a request arena. For example, using [http.zig](https://www.github.com/karlseguin/http.zig):\n\n```zig\ntry template.render(res.writer(), .{}, .{\n    .allocator = res.arena,\n});\n```\n\n## Customization\n`ztl.Template` is a generic. The generic serves two purposes: to configure ztl and to provide custom functions.\n\nFor simple cases, `void` can be passed. \n\nTo configure ZTL, to add custom functions or to support the `@include` builtin, a custom type must be passed.\n\n\n```zig\nconst MyAppsTemplate = struct {\n\n    // Low level configuration\n    pub const ZtlConfig = struct {\n        // default values:\n\n        pub const debug: DebugMode = .none; // .minimal or .ful\n        pub const max_locals: u16 = 256;\n        pub const max_call_frames: u8 = 255;\n        pub const initial_code_size: u32 = 512;\n        pub const initial_data_size: u32 = 512;\n        pub const deduplicate_string_literals: bool = true;\n        pub const escape_by_default: bool = false;\n    };\n\n    // Defines the function and the number of arguments they take\n    // Must also define a `call` method (below)\n    pub const ZtlFunctions = struct {\n        pub const add = 2; // takes 2 parameters\n        pub const double = 1;  // takes 1 parameter\n    };\n\n    // must also have a ZtlFunction struct (above) with each function and their arity\n    pub fn call(self: *MyAppsTemplate, vm: *ztl.VM(*@This()), function: ztl.Functions(@This()), values: []ztl.Value) !ztl.Value {\n        _ = vm;\n\n        switch (function) {\n            .add =\u003e return .{.i64 = values[0].i64 + values[1].i64},\n            .double =\u003e return .{.i64 = values[0].i64 * 2 + self.id},\n        }\n    }\n\n    // see #include documentation\n    pub fn partial(self: *MyAppsTemplate, _: Allocator, template_key: []const u8, include_key: []const u8) !?ztl.PartialResult {\n        _ = template_key;\n        if (std.mem.eql(u8, include_key, \"header\")) {\n            return .{.src = \"Welcome \u003c%= @name %\u003e\"}\n        }\n\n        return null;\n    }\n}\n```\n\n### Extending with Zig\nThe combination of `ZtlFunctions` and the `call` function allows custom Zig code to be exposed as a template function. With the above `MyAppsTemplate`, templates can call `add(a, b)` and `double(123)`.\n\nHowever, do note that the above implementation of `call` is unsafe. The # of parameters is guaranteed to be right, but not the types. In other words, when `function == .add` then `values.len == 2`, but the type of `values[0]` and `values[1]` is not guaranteed to be an `i64`.\n\nThe first parameter to `call` is the instance of `MyAppsTemplate` passed into `Template.init`. This could be used, for example, to write a custom function that can access a database.\n\n## Builtins\nThere are a few built-in functions. \n\n### @print\nThe `@print()` builtin function prints the value(s) to stderr.\n\n```js\n@print(value, 1, \"hello\");\n```\n\n### @include\nA template can `@include` another. \n\n````\n// header\nHello \u003c%= @name %\u003e\n\n// main template\n\u003c% @include('header', %{name: \"Leto\"}) %\u003e\n```\n\nFor this to work, the `partial` method must be defined, as seen above in `MyAppsTemplate`. The `partial` method takes 4 parameters:\n\n* `self` - The instance of T passed into Template.init()\n* `allocator` - An [arena] allocator. For example, if you're going to read the partial using `file.readToEndAlloc`, you should use this allocator.\n* `template_key` - When `template.compile` is called, you can specify a `key: []const u8 = \"\"` in the options. This value is passed back into `partial` (to help you identify which template is being compiled)\n* `include_key` - The first parameter passed to `@include`. In the little snippet above, this would be \"header\".\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkarlseguin%2Fztl","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkarlseguin%2Fztl","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkarlseguin%2Fztl/lists"}