Files
pugz/CLAUDE.md
Ankit Patial 1fff91d7d9 Add string concatenation in attributes, lazy mixin loading, and benchmarks
Features:
- Fix string concatenation in attribute values (e.g., class="btn btn-" + type)
- Lexer now properly captures full expressions with operators
- Runtime evaluates expressions for class attributes

ViewEngine improvements:
- Change mixin loading from eager to lazy (on-demand)
- Mixins are now loaded from mixins directory only when first called
- Template-defined mixins take precedence over directory mixins

Benchmarks:
- Add src/benchmark.zig with three template complexity levels
- Simple: ~150k renders/sec, 6KB memory
- Medium: ~70k renders/sec, 45KB memory
- Complex: ~32k renders/sec, 94KB memory
- Memory leak detection confirms no leaks

Documentation:
- Update CLAUDE.md with lazy mixin loading details
- Document mixin resolution order
2026-01-18 00:05:16 +05:30

9.6 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.

Build Commands

  • zig build - Build the project (output in zig-out/)
  • zig build test - Run all tests
  • zig build app-01 - Run the example web app (http://localhost:8080)

Architecture Overview

The template engine follows a classic compiler pipeline:

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

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/codegen.zig Static HTML generation (without runtime evaluation). Outputs placeholders for dynamic content.
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() and core types.

Test Files

  • src/tests/general_test.zig - Comprehensive integration tests for all features

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

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

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

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}

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

The ViewEngine provides the simplest API for web servers:

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)
  • Conditionals (if/else if/else/unless)
  • Iteration (each with index, else branch, objects)
  • 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)

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
  • Compile-time template validation