Files
pugz/CLAUDE.md

13 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Purpose

Pugz is a Pug-like HTML template engine written in Zig 0.15.2. It implements Pug 3 syntax for indentation-based HTML templating with a focus on server-side rendering.

Rules

  • Do not auto commit, user will do it.
  • At the start of each new session, read this CLAUDE.md file to understand project context and rules.
  • When the user specifies a new rule, update this CLAUDE.md file to include it.

Build Commands

  • zig build - Build the project (output in zig-out/)
  • zig build test - Run all tests
  • zig build bench-compiled - Run compiled templates benchmark (compare with Pug.js)
  • zig build bench-interpreted - Inpterpret trmplates

Architecture Overview

The template engine supports two rendering modes:

1. Runtime Rendering (Interpreted)

Source → Lexer → Tokens → Parser → AST → Runtime → HTML

2. Build-Time Compilation (Compiled)

Source → Lexer → Tokens → Parser → AST → build_templates.zig → generated.zig → Native Zig Code

The compiled mode is ~3x faster than Pug.js.

Core Modules

Module Purpose
src/lexer.zig Tokenizes Pug source into tokens. Handles indentation tracking, raw text blocks, interpolation.
src/parser.zig Converts token stream into AST. Handles nesting via indent/dedent tokens.
src/ast.zig AST node definitions (Element, Text, Conditional, Each, Mixin, etc.)
src/runtime.zig Evaluates AST with data context, produces final HTML. Handles variable interpolation, conditionals, loops, mixins.
src/build_templates.zig Build-time template compiler. Generates optimized Zig code from .pug templates.
src/view_engine.zig High-level ViewEngine for web servers. Manages views directory, auto-loads mixins.
src/root.zig Public library API - exports ViewEngine, renderTemplate(), build_templates and core types.

Test Files

  • src/tests/general_test.zig - Comprehensive integration tests for all features
  • src/tests/doctype_test.zig - Doctype-specific tests
  • src/tests/inheritance_test.zig - Template inheritance tests

Build-Time Template Compilation

For maximum performance, templates can be compiled to native Zig code at build time.

Setup in build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const pugz_dep = b.dependency("pugz", .{});
    
    // Compile templates at build time
    const build_templates = @import("pugz").build_templates;
    const compiled_templates = build_templates.compileTemplates(b, .{
        .source_dir = "views",  // Directory containing .pug files
    });

    const exe = b.addExecutable(.{
        .name = "myapp",
        .root_module = b.createModule(.{
            .root_source_file = b.path("src/main.zig"),
            .imports = &.{
                .{ .name = "pugz", .module = pugz_dep.module("pugz") },
                .{ .name = "tpls", .module = compiled_templates },
            },
        }),
    });
}

Usage in Code

const tpls = @import("tpls");

pub fn handleRequest(allocator: std.mem.Allocator) ![]u8 {
    // Zero-cost template rendering - just native Zig code
    return try tpls.home(allocator, .{
        .title = "Welcome",
        .user = .{ .name = "Alice", .email = "alice@example.com" },
        .items = &[_][]const u8{ "One", "Two", "Three" },
    });
}

Generated Code Features

The compiler generates optimized Zig code with:

  • Static string merging - Consecutive static content merged into single appendSlice calls
  • Zero allocation for static templates - Returns string literal directly
  • Type-safe data access - Uses @field(d, "name") for compile-time checked field access
  • Automatic type conversion - strVal() helper converts integers to strings
  • Optional handling - Nullable slices handled with orelse &.{}
  • HTML escaping - Lookup table for fast character escaping

Benchmark Results (2000 iterations)

Template Pug.js Pugz Speedup
simple-0 0.8ms 0.1ms 8x
simple-1 1.4ms 0.6ms 2.3x
simple-2 1.8ms 0.6ms 3x
if-expression 0.6ms 0.2ms 3x
projects-escaped 4.4ms 0.6ms 7.3x
search-results 15.2ms 5.6ms 2.7x
friends 153.5ms 54.0ms 2.8x
TOTAL 177.6ms 61.6ms ~3x faster

Run benchmarks:

# Pugz (Zig)
zig build bench-compiled

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

Memory Management

Important: The runtime is designed to work with ArenaAllocator:

var arena = std.heap.ArenaAllocator.init(allocator);
defer arena.deinit();  // Frees all template memory at once

const html = try pugz.renderTemplate(arena.allocator(), template, data);

This pattern is recommended because template rendering creates many small allocations that are all freed together after the response is sent.

Key Implementation Details

Lexer State Machine

The lexer tracks several states for handling complex syntax:

  • in_raw_block / raw_block_indent / raw_block_started - For dot block text (e.g., script.)
  • indent_stack - Stack-based indent/dedent token generation

Important: The lexer distinguishes between #id (ID selector), #{expr} (interpolation), and #[tag] (tag interpolation) by looking ahead at the next character.

Token Types

Key tokens: tag, class, id, lparen, rparen, attr_name, attr_value, text, interp_start, interp_end, indent, dedent, dot_block, pipe_text, literal_html, self_close, mixin_call, etc.

AST Node Types

  • element - HTML elements with tag, classes, id, attributes, children
  • text - Text with segments (literal, escaped interpolation, unescaped interpolation, tag interpolation)
  • conditional - if/else if/else/unless branches
  • each - Iteration with value, optional index, else branch
  • mixin_def / mixin_call - Mixin definitions and invocations
  • block - Named blocks for template inheritance
  • include / extends - File inclusion and inheritance
  • raw_text - Literal HTML or text blocks

Runtime Value System

pub const Value = union(enum) {
    null,
    bool: bool,
    int: i64,
    float: f64,
    string: []const u8,
    array: []const Value,
    object: std.StringHashMapUnmanaged(Value),
};

The toValue() function converts Zig structs to runtime Values automatically.

Supported Pug Features

Tags & Nesting

div
  h1 Title
  p Paragraph

Classes & IDs (shorthand)

div#main.container.active
.box        // defaults to div
#sidebar    // defaults to div

Attributes

a(href="/link" target="_blank") Click
input(type="checkbox" checked)
div(style={color: 'red'})
div(class=['foo', 'bar'])
button(disabled=false)      // omitted when false
button(disabled=true)       // disabled="disabled"

Text & Interpolation

p Hello #{name}             // escaped interpolation
p Hello !{rawHtml}          // unescaped interpolation
p= variable                 // buffered code (escaped)
p!= rawVariable             // buffered code (unescaped)
| Piped text line
p.
  Multi-line
  text block
<p>Literal HTML</p>         // passed through as-is

// Interpolation-only text works too
h1.header #{title}          // renders <h1 class="header">Title Value</h1>

Tag Interpolation

p This is #[em emphasized] text
p Click #[a(href="/") here] to continue

Block Expansion

a: img(src="logo.png")      // colon for inline nesting

Explicit Self-Closing

foo/                        // renders as <foo />

Conditionals

if condition
  p Yes
else if other
  p Maybe
else
  p No

unless loggedIn
  p Please login

// String comparison in conditions
if status == "active"
  p Active

Iteration

each item in items
  li= item

each val, index in list
  li #{index}: #{val}

each item in items
  li= item
else
  li No items

// Works with objects too (key as index)
each val, key in object
  p #{key}: #{val}

// Nested iteration with field access
each friend in friends
  li #{friend.name}
  each tag in friend.tags
    span= tag

Case/When

case status
  when "active"
    p Active
  when "pending"
    p Pending
  default
    p Unknown

Mixins

mixin button(text, type="primary")
  button(class="btn btn-" + type)= text

+button("Click me")
+button("Submit", "success")

// With block content
mixin card(title)
  .card
    h3= title
    block

+card("My Card")
  p Card content here

// Rest arguments
mixin list(id, ...items)
  ul(id=id)
    each item in items
      li= item

+list("mylist", "a", "b", "c")

// Attributes pass-through
mixin link(href, text)
  a(href=href)&attributes(attributes)= text

+link("/home", "Home")(class="nav-link" data-id="1")

Includes & Inheritance

include header.pug

extends layout.pug
block content
  h1 Page Title

// Block modes
block append scripts
  script(src="extra.js")

block prepend styles
  link(rel="stylesheet" href="extra.css")

Comments

// This renders as HTML comment
//- This is a silent comment (not in output)

Server Usage

Use build-time compilation for best performance. See "Build-Time Template Compilation" section above.

ViewEngine (Runtime Rendering)

The ViewEngine provides runtime template rendering with lazy-loading:

const std = @import("std");
const pugz = @import("pugz");

// Initialize once at server startup
var engine = try pugz.ViewEngine.init(allocator, .{
    .views_dir = "src/views",     // Root views directory
    .mixins_dir = "mixins",       // Mixins dir for lazy-loading (optional, default: "mixins")
    .extension = ".pug",          // File extension (default: .pug)
    .pretty = true,               // Pretty-print output (default: true)
});
defer engine.deinit();

// In request handler - use arena allocator per request
pub fn handleRequest(engine: *pugz.ViewEngine, allocator: std.mem.Allocator) ![]u8 {
    var arena = std.heap.ArenaAllocator.init(allocator);
    defer arena.deinit();

    // Template path is relative to views_dir, extension added automatically
    return try engine.render(arena.allocator(), "pages/home", .{
        .title = "Home",
        .user = .{ .name = "Alice" },
    });
}

Mixin Resolution (Lazy Loading)

Mixins are resolved in the following order:

  1. Same template - Mixins defined in the current template file
  2. Mixins directory - If not found, searches views/mixins/*.pug files (lazy-loaded on first use)

This lazy-loading approach means:

  • Mixins are only parsed when first called
  • No upfront loading of all mixin files at server startup
  • Templates can override mixins from the mixins directory by defining them locally

Directory Structure

src/views/
├── mixins/           # Lazy-loaded mixins (searched when mixin not found in template)
│   ├── buttons.pug   # mixin btn(text), mixin btn-link(href, text)
│   └── cards.pug     # mixin card(title), mixin card-simple(title, body)
├── layouts/
│   └── base.pug      # Base layout with blocks
├── partials/
│   ├── header.pug
│   └── footer.pug
└── pages/
    ├── home.pug      # extends layouts/base
    └── about.pug     # extends layouts/base

Templates can use:

  • extends layouts/base - Paths relative to views_dir
  • include partials/header - Paths relative to views_dir
  • +btn("Click") - Mixins from mixins/ dir loaded on-demand

Low-Level API

For inline templates or custom use cases:

const std = @import("std");
const pugz = @import("pugz");

pub fn handleRequest(allocator: std.mem.Allocator) ![]u8 {
    var arena = std.heap.ArenaAllocator.init(allocator);
    defer arena.deinit();

    return try pugz.renderTemplate(arena.allocator(),
        \\html
        \\  head
        \\    title= title
        \\  body
        \\    h1 Hello, #{name}!
        \\    if showList
        \\      ul
        \\        each item in items
        \\          li= item
    , .{
        .title = "My Page",
        .name = "World",
        .showList = true,
        .items = &[_][]const u8{ "One", "Two", "Three" },
    });
}

Testing

Run tests with zig build test. Tests cover:

  • Basic element parsing and rendering
  • Class and ID shorthand syntax
  • Attribute parsing (quoted, unquoted, boolean, object literals)
  • Text interpolation (escaped, unescaped, tag interpolation)
  • Interpolation-only text (e.g., h1.class #{var})
  • Conditionals (if/else if/else/unless)
  • Iteration (each with index, else branch, objects, nested loops)
  • Case/when statements
  • Mixin definitions and calls (with defaults, rest args, block content, attributes)
  • Plain text (piped, dot blocks, literal HTML)
  • Self-closing tags (void elements, explicit /)
  • Block expansion with colon
  • Comments (rendered and silent)
  • String comparison in conditions

Error Handling

The lexer and parser return errors for invalid syntax:

  • ParserError.UnexpectedToken
  • ParserError.MissingCondition
  • ParserError.MissingMixinName
  • RuntimeError.ParseError (wrapped for convenience API)

Future Improvements

Potential areas for enhancement:

  • Filter support (:markdown, :stylus, etc.)
  • More complete JavaScript expression evaluation
  • Source maps for debugging
  • Mixin support in compiled templates