Files
pugz/docs/CLAUDE.md
Ankit Patial 416ddf5b33 fix: merge multiple class attributes into single attribute
- codegen.zig: collect class values and output as single merged attribute
- template.zig: respect quoted flag to prevent data lookup for static class values
- Added tests for multiple class merging scenarios
2026-01-29 22:04:59 +05:30

14 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 compiles Pug templates directly to HTML (unlike JS pug which compiles to JavaScript functions). 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.
  • Code comments are required but must be meaningful, not bloated. Focus on explaining "why" not "what". Avoid obvious comments like "// increment counter" - instead explain complex logic, non-obvious decisions, or tricky edge cases.
  • All documentation files (.md) must be saved to the docs/ directory. Do not create .md files in the root directory or examples directories - always place them in docs/.
  • Publish command: When user says "publish", do the following:
    1. Bump the fix version (patch version in build.zig.zon)
    2. Git commit with appropriate message
    3. Git push to remote origin and remote github

Build Commands

  • zig build - Build the project (output in zig-out/)
  • zig build test - Run all tests
  • zig build test-compile - Test the template compilation build step
  • zig build bench-v1 - Run v1 template benchmark
  • zig build bench-interpreted - Run interpreted templates benchmark

Architecture Overview

Compilation Pipeline

Source → Lexer → Tokens → StripComments → Parser → AST → Linker → Codegen → HTML

Three Rendering Modes

  1. Static compilation (pug.compile): Outputs HTML directly
  2. Data binding (template.renderWithData): Supports #{field} interpolation with Zig structs
  3. Compiled templates (.pug.zig): Pre-compile templates to Zig functions for maximum performance

Core Modules

Module File Purpose
Lexer src/lexer.zig Tokenizes Pug source into tokens
Parser src/parser.zig Builds AST from tokens
Runtime src/runtime.zig Shared utilities (HTML escaping, etc.)
Error src/error.zig Error formatting with source context
Walk src/walk.zig AST traversal with visitor pattern
Strip Comments src/strip_comments.zig Token filtering for comments
Load src/load.zig File loading for includes/extends
Linker src/linker.zig Template inheritance (extends/blocks)
Codegen src/codegen.zig AST to HTML generation
Template src/template.zig Data binding renderer
Pug src/pug.zig Main entry point
ViewEngine src/view_engine.zig High-level API for web servers
ZigCodegen src/tpl_compiler/zig_codegen.zig Compiles .pug AST to Zig functions
CompileTpls src/compile_tpls.zig Build step for compiling templates at build time
Root src/root.zig Public library API exports

Test Files

  • tests/general_test.zig - Comprehensive integration tests
  • tests/doctype_test.zig - Doctype-specific tests
  • tests/check_list_test.zig - Template output validation tests

API Usage

Static Compilation (no data)

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

pub fn main() !void {
    const allocator = std.heap.page_allocator;

    var result = try pug.compile(allocator, "p Hello World", .{});
    defer result.deinit(allocator);

    std.debug.print("{s}\n", .{result.html}); // <p>Hello World</p>
}

Dynamic Rendering with Data

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

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();

    const html = try pugz.renderTemplate(arena.allocator(),
        \\h1 #{title}
        \\p #{message}
    , .{
        .title = "Welcome",
        .message = "Hello, World!",
    });

    std.debug.print("{s}\n", .{html});
    // Output: <h1>Welcome</h1><p>Hello, World!</p>
}

Data Binding Features

  • Interpolation: #{fieldName} in text content
  • Attribute binding: a(href=url) binds url field to href
  • Buffered code: p= message outputs the message field
  • Auto-escaping: HTML is escaped by default (XSS protection)
const html = try pugz.renderTemplate(allocator,
    \\a(href=url, class=style) #{text}
, .{
    .url = "https://example.com",
    .style = "btn",
    .text = "Click me!",
});
// Output: <a href="https://example.com" class="btn">Click me!</a>

Compiled Templates (Maximum Performance)

For production deployments where maximum performance is critical, you can pre-compile .pug templates to Zig functions using a build step:

Step 1: Add build step to your build.zig

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    // Add pugz dependency
    const pugz_dep = b.dependency("pugz", .{
        .target = target,
        .optimize = optimize,
    });
    const pugz = pugz_dep.module("pugz");

    // Add template compilation build step
    const compile_templates = @import("pugz").addCompileStep(b, .{
        .name = "compile-templates",
        .source_dirs = &.{"src/views", "src/pages"},  // Can specify multiple directories
        .output_dir = "generated",
    });

    const exe = b.addExecutable(.{
        .name = "myapp",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    exe.root_module.addImport("pugz", pugz);
    exe.root_module.addImport("templates", compile_templates.getOutput());
    exe.step.dependOn(&compile_templates.step);

    b.installArtifact(exe);
}

Step 2: Use compiled templates in your code

const std = @import("std");
const tpls = @import("templates");  // Import from build step

pub fn handleRequest(allocator: std.mem.Allocator) ![]const u8 {
    // Access templates by their path: views/pages/home.pug -> tpls.views_pages_home
    return try tpls.views_home.render(allocator, .{
        .title = "Home",
        .name = "Alice",
    });
}

// Or use layouts
pub fn renderLayout(allocator: std.mem.Allocator) ![]const u8 {
    return try tpls.layouts_base.render(allocator, .{
        .content = "Main content here",
    });
}

How templates are named:

  • views/home.pugtpls.views_home
  • pages/about.pugtpls.pages_about
  • layouts/main.pugtpls.layouts_main
  • views/user-profile.pugtpls.views_user_profile (dashes become underscores)
  • Directory separators and dashes are converted to underscores

Performance Benefits:

  • Zero parsing overhead - templates compiled at build time
  • Type-safe data binding - compile errors for missing fields
  • Optimized code - direct string concatenation instead of AST traversal
  • ~10-100x faster than runtime parsing depending on template complexity

What gets resolved at compile time:

  • Template inheritance (extends/block) - fully resolved
  • Includes (include) - inlined into template
  • Mixins - available in compiled templates

Trade-offs:

  • Templates are regenerated automatically when you run zig build
  • Includes/extends are resolved at compile time (no dynamic loading)
  • Each/if statements not yet supported (coming soon)

ViewEngine (for Web Servers)

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

const engine = pugz.ViewEngine.init(.{
    .views_dir = "src/views",
    .extension = ".pug",
});

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

    return try engine.render(arena.allocator(), "pages/home", .{
        .title = "Home",
        .user = .{ .name = "Alice" },
    });
}

Compile Options

pub const CompileOptions = struct {
    filename: ?[]const u8 = null,        // For error messages
    basedir: ?[]const u8 = null,         // For absolute includes
    pretty: bool = false,                 // Pretty print output
    strip_unbuffered_comments: bool = true,
    strip_buffered_comments: bool = false,
    debug: bool = false,
    doctype: ?[]const u8 = null,
};

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 Notes

Lexer (lexer.zig)

  • Lexer.init(allocator, source, options) - Initialize
  • Lexer.getTokens() - Returns token slice
  • Lexer.last_error - Check for errors after failed getTokens()

Parser (parser.zig)

  • Parser.init(allocator, tokens, filename, source) - Initialize
  • Parser.parse() - Returns AST root node
  • Parser.err - Check for errors after failed parse()

Codegen (codegen.zig)

  • Compiler.init(allocator, options) - Initialize
  • Compiler.compile(ast) - Returns HTML string

Walk (walk.zig)

  • Uses O(1) stack operations (append/pop) not O(n) insert/remove
  • getParent(index) uses reverse indexing (0 = immediate parent)
  • initWithCapacity() for pre-allocation optimization

Runtime (runtime.zig)

  • escapeChar(c) - Shared HTML escape function
  • appendEscaped(list, allocator, str) - Append with escaping

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 (SAFE - default)
p Hello !{rawHtml}          // unescaped interpolation (UNSAFE - trusted content only)
p= variable                 // buffered code (escaped, SAFE)
p!= rawVariable             // buffered code (unescaped, UNSAFE)
| Piped text line
p.
  Multi-line
  text block
<p>Literal HTML</p>         // passed through as-is

Security Note: By default, #{} and = escape HTML entities (<, >, &, ", ') to prevent XSS attacks. Only use !{} or != for content you fully trust.

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

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

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")

Includes & Inheritance

include header.pug

extends layout.pug
block content
  h1 Page Title

Comments

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

Benchmark Results (2000 iterations)

Template Time
simple-0 0.8ms
simple-1 11.6ms
simple-2 8.2ms
if-expression 7.4ms
projects-escaped 7.1ms
search-results 13.4ms
friends 22.9ms
TOTAL 71.3ms

Limitations vs JS Pug

  1. No JavaScript expressions: - var x = 1 not supported
  2. No nested field access: #{user.name} not supported, only #{name}
  3. No filters: :markdown, :coffee etc. not implemented
  4. String fields only: Data binding works best with []const u8 fields

Error Handling

Uses error unions with detailed PugError context including line, column, and source snippet:

  • LexerError - Tokenization errors
  • ParserError - Syntax errors
  • ViewEngineError - Template not found, parse errors

File Structure

├── src/                    # Source code
│   ├── root.zig            # Public library API
│   ├── view_engine.zig     # High-level ViewEngine
│   ├── pug.zig             # Main entry point (static compilation)
│   ├── template.zig        # Data binding renderer
│   ├── compile_tpls.zig    # Build step for template compilation
│   ├── lexer.zig           # Tokenizer
│   ├── parser.zig          # AST parser
│   ├── runtime.zig         # Shared utilities
│   ├── error.zig           # Error formatting
│   ├── walk.zig            # AST traversal
│   ├── strip_comments.zig  # Comment filtering
│   ├── load.zig            # File loading
│   ├── linker.zig          # Template inheritance
│   ├── codegen.zig         # HTML generation
│   └── tpl_compiler/       # Template-to-Zig code generation
│       ├── zig_codegen.zig     # AST to Zig function compiler
│       ├── main.zig            # CLI tool (standalone)
│       └── helpers_template.zig # Runtime helpers template
├── tests/              # Integration tests
│   ├── general_test.zig
│   ├── doctype_test.zig
│   └── check_list_test.zig
├── benchmarks/         # Performance benchmarks
├── docs/               # Documentation
├── examples/           # Example templates
└── playground/         # Development playground