- Lexer now emits start_pug_interpolation/end_pug_interpolation tokens - Sub-lexer parses content inside #[...] as full Pug syntax - Supports tags with attributes, classes, IDs, and buffered code - Added tag_interp_test.zig with 8 test cases - Memory management: sub-lexer buffers tracked and freed properly Examples now working: p Dear #[strong= "asdasd"] -> <p>Dear <strong>asdasd</strong></p> p This is #[em emphasized] text -> <p>This is <em>emphasized</em> text</p> p Click #[a(href='/') here] -> <p>Click <a href="/">here</a></p>
Pugz
A Pug template engine written in Zig. Templates are parsed and rendered with data at runtime.
Features
- Pug syntax (tags, classes, IDs, attributes)
- Interpolation (
#{var},!{unescaped}) - Conditionals (
if,else if,else,unless) - Iteration (
each,while) - Template inheritance (
extends,block,append,prepend) - Includes
- Mixins with parameters, defaults, rest args, and block content
- Comments (rendered and unbuffered)
- Pretty printing with indentation
Installation
Add pugz as a dependency in your build.zig.zon:
zig fetch --save "git+https://github.com/ankitpatial/pugz#main"
Then in your build.zig:
const pugz_dep = b.dependency("pugz", .{
.target = target,
.optimize = optimize,
});
exe.root_module.addImport("pugz", pugz_dep.module("pugz"));
Usage
ViewEngine
The ViewEngine provides file-based template management for web servers.
const std = @import("std");
const pugz = @import("pugz");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
// Initialize once at server startup
var engine = pugz.ViewEngine.init(.{
.views_dir = "views",
});
defer engine.deinit();
// Per-request rendering with arena allocator
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const html = try engine.render(arena.allocator(), "pages/index", .{
.title = "Hello",
.name = "World",
});
std.debug.print("{s}\n", .{html});
}
Inline Templates
For simple use cases or testing, render template strings directly:
const html = try pugz.renderTemplate(allocator,
\\h1 Hello, #{name}!
\\ul
\\ each item in items
\\ li= item
, .{
.name = "World",
.items = &[_][]const u8{ "one", "two", "three" },
});
With http.zig
const pugz = @import("pugz");
const httpz = @import("httpz");
var engine: pugz.ViewEngine = undefined;
pub fn main() !void {
engine = pugz.ViewEngine.init(.{
.views_dir = "views",
});
defer engine.deinit();
var server = try httpz.Server(*Handler).init(allocator, .{}, handler);
try server.listen();
}
fn handler(_: *Handler, _: *httpz.Request, res: *httpz.Response) !void {
res.content_type = .HTML;
res.body = try engine.render(res.arena, "pages/home", .{
.title = "Hello",
.user = .{ .name = "Alice" },
});
}
Compiled Templates (Maximum Performance)
For production deployments, pre-compile .pug templates to Zig functions at build time. This eliminates parsing overhead and provides type-safe data binding.
Step 1: Update your build.zig
const std = @import("std");
const pugz = @import("pugz");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const pugz_dep = b.dependency("pugz", .{
.target = target,
.optimize = optimize,
});
// Add template compilation step
const compile_templates = pugz.compile_tpls.addCompileStep(b, .{
.name = "compile-templates",
.source_dirs = &.{"views/pages", "views/partials"},
.output_dir = "generated",
});
// Templates module from compiled output
const templates_mod = b.createModule(.{
.root_source_file = compile_templates.getOutput(),
});
const exe = b.addExecutable(.{
.name = "myapp",
.root_module = b.createModule(.{
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
.imports = &.{
.{ .name = "pugz", .module = pugz_dep.module("pugz") },
.{ .name = "templates", .module = templates_mod },
},
}),
});
// Ensure templates compile before building
exe.step.dependOn(&compile_templates.step);
b.installArtifact(exe);
}
Step 2: Use compiled templates
const templates = @import("templates");
fn handler(res: *httpz.Response) !void {
res.content_type = .HTML;
res.body = try templates.pages_home.render(res.arena, .{
.title = "Home",
.name = "Alice",
});
}
Template naming:
views/pages/home.pug→templates.pages_homeviews/pages/product-detail.pug→templates.pages_product_detail- Directory separators and dashes become underscores
Benefits:
- Zero parsing overhead at runtime
- Type-safe data binding with compile-time errors
- Template inheritance (
extends/block) fully resolved at build time
Current limitations:
each/ifstatements not yet supported in compiled mode- All data fields must be
[]const u8
See examples/demo/ for a complete working example.
ViewEngine Options
var engine = pugz.ViewEngine.init(.{
.views_dir = "views", // Root directory for templates
.extension = ".pug", // File extension (default: .pug)
.pretty = false, // Enable pretty-printed output
});
| Option | Default | Description |
|---|---|---|
views_dir |
"views" |
Root directory containing templates |
extension |
".pug" |
File extension for templates |
pretty |
false |
Enable pretty-printed HTML with indentation |
Memory Management
Always use an ArenaAllocator for rendering. Template rendering creates many small allocations that should be freed together after the response is sent.
var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();
const html = try engine.render(arena.allocator(), "index", data);
Documentation
- Template Syntax - Complete syntax reference
- API Reference - Detailed API documentation
Benchmarks
Same templates and data (src/tests/benchmarks/templates/), MacBook Air M2, 2000 iterations, best of 5 runs.
Benchmark Modes
| Mode | Description |
|---|---|
| Pug.js | Node.js Pug - compile once, render many |
| Prerender | Pugz - parse + render every iteration (no caching) |
| Cached | Pugz - parse once, render many (like Pug.js) |
| Compiled | Pugz - pre-compiled to Zig functions (zero parse overhead) |
Results
| Template | Pug.js | Prerender | Cached | Compiled |
|---|---|---|---|---|
| simple-0 | 0.8ms | 23.1ms | 132.3µs | 15.9µs |
| simple-1 | 1.5ms | 33.5ms | 609.3µs | 17.3µs |
| simple-2 | 1.7ms | 38.4ms | 936.8µs | 17.8µs |
| if-expression | 0.6ms | 28.8ms | 23.0µs | 15.5µs |
| projects-escaped | 4.6ms | 34.2ms | 1.2ms | 15.8µs |
| search-results | 15.3ms | 34.0ms | 43.5µs | 15.6µs |
| friends | 156.7ms | 34.7ms | 739.0µs | 16.8µs |
| TOTAL | 181.3ms | 227.7ms | 3.7ms | 114.8µs |
Compiled templates are ~32x faster than cached and ~2000x faster than prerender.
Run Benchmarks
# Pugz (all modes)
zig build bench
# Pug.js (for comparison)
cd src/tests/benchmarks/pugjs && npm install && npm run bench
Development
zig build test # Run all tests
zig build bench # Run benchmarks
License
MIT