Ankit Patial aaf6a1af2d fix: add scoped error logging for lexer/parser errors
- Add std.log.scoped(.pugz) to template.zig and view_engine.zig
- Log detailed error info (code, line, column, message) when parsing fails
- Log template path context in ViewEngine on parse errors
- Remove debug print from lexer, use proper scoped logging instead
- Move benchmarks, docs, examples, playground, tests out of src/ to project root
- Update build.zig and documentation paths accordingly
- Bump version to 0.3.1
2026-01-25 17:10:02 +05:30
2026-01-24 23:53:19 +05:30
2026-01-17 20:07:55 +05:30

Pugz

A Pug template engine written in Zig. Templates are parsed once and cached, then 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
  • LRU cache with configurable size and TTL

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

The ViewEngine provides template caching and 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 = try pugz.ViewEngine.init(allocator, .{
        .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 = try pugz.ViewEngine.init(allocator, .{
        .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" },
    });
}

ViewEngine Options

var engine = try pugz.ViewEngine.init(allocator, .{
    .views_dir = "views",           // Root directory for templates
    .extension = ".pug",            // File extension (default: .pug)
    .pretty = false,                // Enable pretty-printed output
    .cache_enabled = true,          // Enable AST caching
    .max_cached_templates = 100,    // LRU cache size (0 = unlimited)
    .cache_ttl_seconds = 5,         // Cache TTL for development (0 = never expires)
});
Option Default Description
views_dir "views" Root directory containing templates
extension ".pug" File extension for templates
pretty false Enable pretty-printed HTML with indentation
cache_enabled true Enable AST caching for performance
max_cached_templates 0 Max templates in LRU cache (0 = unlimited hashmap)
cache_ttl_seconds 0 Cache TTL in seconds (0 = never expires)

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


Benchmarks

Same templates and data (benchmarks/templates/), MacBook Air M2, 2000 iterations, best of 5 runs.

Both Pug.js and Pugz parse templates once, then measure render-only time.

Template Pug.js Pugz Speedup
simple-0 0.8ms 0.2ms 4x
simple-1 1.5ms 0.9ms 1.7x
simple-2 1.7ms 2.4ms 0.7x
if-expression 0.6ms 0.4ms 1.5x
projects-escaped 4.6ms 2.4ms 1.9x
search-results 15.3ms 17.7ms 0.9x
friends 156.7ms 132.2ms 1.2x
TOTAL 181.3ms 156.2ms 1.16x

Run benchmarks:

# Pugz
zig build bench

# Pug.js (for comparison)
cd benchmarks/pugjs && npm install && npm run bench

Development

zig build test    # Run all tests
zig build bench   # Run benchmarks

License

MIT

Description
No description provided
Readme MIT 2.2 MiB
Languages
Zig 74.1%
JavaScript 11.9%
Pug 9.3%
HTML 4.7%